From fbf8497c59c63e633b7c118803c76c313773a692 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Fri, 31 Oct 2025 00:53:47 -0700 Subject: [PATCH 1/6] fix: Fix PlantSystem serialization of LeftSeeds and LeftResources at zero (#2255) * Initial plan * Add SerializableFieldDefault attributes for LeftSeeds and LeftResources This fixes the serialization bug where _leftSeeds and _leftResources were initialized to 8 in the constructor but didn't serialize when their value was 0. Upon deserialization, the constructor would run again and reset these values back to 8. The SerializableFieldDefault attributes tell the serialization system that the default value is 8, so it will properly serialize 0 values and maintain the correct state across server restarts. Co-authored-by: kamronbatman <3953314+kamronbatman@users.noreply.github.com> * Update ShouldSerialize methods to check against default value Changed ShouldSerializeLeftSeeds() and ShouldSerializeLeftResources() to check if the value is != 8 (the default) instead of != 0. This ensures that only non-default values are serialized, following the same pattern used in BaseWeapon.cs and other classes with SerializableFieldDefault attributes. Co-authored-by: kamronbatman <3953314+kamronbatman@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: kamronbatman <3953314+kamronbatman@users.noreply.github.com> --- Projects/UOContent/Engines/Plants/PlantSystem.cs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/Projects/UOContent/Engines/Plants/PlantSystem.cs b/Projects/UOContent/Engines/Plants/PlantSystem.cs index 61d017c0f..cbbd0cb9c 100644 --- a/Projects/UOContent/Engines/Plants/PlantSystem.cs +++ b/Projects/UOContent/Engines/Plants/PlantSystem.cs @@ -320,7 +320,10 @@ namespace Server.Engines.Plants } [SerializableFieldSaveFlag(17)] - private bool ShouldSerializeLeftSeeds() => _leftSeeds != 0; + private bool ShouldSerializeLeftSeeds() => _leftSeeds != 8; + + [SerializableFieldDefault(17)] + private int LeftSeedsDefaultValue() => 8; [SerializableProperty(18)] public int AvailableResources @@ -340,7 +343,10 @@ namespace Server.Engines.Plants } [SerializableFieldSaveFlag(19)] - private bool ShouldSerializeLeftResources() => _leftResources != 0; + private bool ShouldSerializeLeftResources() => _leftResources != 8; + + [SerializableFieldDefault(19)] + private int LeftResourcesDefaultValue() => 8; public void Reset(bool potions) { From 3a3f5ee518ea37f1c23996ca7ffe87e8e3c5f725 Mon Sep 17 00:00:00 2001 From: Kamron Batman <3953314+kamronbatman@users.noreply.github.com> Date: Tue, 11 Nov 2025 11:26:34 -0800 Subject: [PATCH 2/6] feat: Adds .NET 10 / C# 14 support. (#2258) ### Summary * Adds .NET 10 support * Bumps to C# 14 --- .config/dotnet-tools.json | 2 +- .github/workflows/build-test.yml | 4 +- .github/workflows/create-release.yml | 2 +- Directory.Build.props | 10 +- Projects/Server.Tests/Server.Tests.csproj | 4 +- Projects/Server/Random/BuiltInSecureRng.cs | 28 ------ Projects/Server/Server.csproj | 4 +- .../UOContent.Tests/UOContent.Tests.csproj | 4 +- .../Accounting/Security/AccountSecurity.cs | 91 +++++++++---------- .../Security/Argon2PasswordProtection.cs | 21 ++--- .../HashAlgorithmPasswordProtection.cs | 35 ++++--- .../Security/PBKDF2PasswordProtection.cs | 59 ++++++------ Projects/UOContent/UOContent.csproj | 8 +- azure-pipelines.yml | 2 +- global.json | 2 +- version.json | 2 +- 16 files changed, 124 insertions(+), 154 deletions(-) delete mode 100644 Projects/Server/Random/BuiltInSecureRng.cs diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json index 08b49e256..5e8c8ad9b 100644 --- a/.config/dotnet-tools.json +++ b/.config/dotnet-tools.json @@ -3,7 +3,7 @@ "isRoot": true, "tools": { "modernuoschemagenerator": { - "version": "2.12.20", + "version": "2.13.0", "commands": [ "ModernUOSchemaGenerator" ] diff --git a/.github/workflows/build-test.yml b/.github/workflows/build-test.yml index c2f1c3b2f..0a906a743 100644 --- a/.github/workflows/build-test.yml +++ b/.github/workflows/build-test.yml @@ -23,7 +23,7 @@ jobs: - uses: actions/checkout@v4 with: fetch-depth: 0 # avoid shallow clone so nbgv can do its work. - - name: Install .NET 9 + - name: Install .NET uses: actions/setup-dotnet@v4 with: global-json-file: global.json @@ -86,7 +86,7 @@ jobs: - uses: actions/checkout@v4 with: fetch-depth: 0 # avoid shallow clone so nbgv can do its work. - - name: Install .NET 9 + - name: Install .NET uses: actions/setup-dotnet@v4 with: global-json-file: global.json diff --git a/.github/workflows/create-release.yml b/.github/workflows/create-release.yml index 0e8334e59..aa02e4489 100644 --- a/.github/workflows/create-release.yml +++ b/.github/workflows/create-release.yml @@ -14,7 +14,7 @@ jobs: with: fetch-depth: 0 # avoid shallow clone so nbgv can do its work. token: ${{ secrets.PERSONAL_ACCESS_TOKEN }} - - name: Install .NET 9 + - name: Install .NET uses: actions/setup-dotnet@v4 with: global-json-file: global.json diff --git a/Directory.Build.props b/Directory.Build.props index ae71e92d4..57f39a926 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -3,9 +3,9 @@ Kamron Batman ModernUO - 2019-2024 - net9.0 - 13 + 2019-2025 + net10.0 + 14 true true NU1603 @@ -64,9 +64,9 @@ - + - 3.7.115 + 3.9.50 all diff --git a/Projects/Server.Tests/Server.Tests.csproj b/Projects/Server.Tests/Server.Tests.csproj index 038575c90..0f913dd48 100644 --- a/Projects/Server.Tests/Server.Tests.csproj +++ b/Projects/Server.Tests/Server.Tests.csproj @@ -5,9 +5,9 @@ Server.Tests - + - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/Projects/Server/Random/BuiltInSecureRng.cs b/Projects/Server/Random/BuiltInSecureRng.cs deleted file mode 100644 index dec408338..000000000 --- a/Projects/Server/Random/BuiltInSecureRng.cs +++ /dev/null @@ -1,28 +0,0 @@ -/************************************************************************* - * ModernUO * - * Copyright 2019-2023 - ModernUO Development Team * - * Email: hi@modernuo.com * - * File: SecureRandom.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.CompilerServices; -using System.Security.Cryptography; - -namespace Server; - -public static class BuiltInSecureRng -{ - public static RandomNumberGenerator Generator { get; } = RandomNumberGenerator.Create(); - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void NextBytes(Span buffer) => Generator.GetBytes(buffer); -} diff --git a/Projects/Server/Server.csproj b/Projects/Server/Server.csproj index 5e16cde20..d9da8166d 100644 --- a/Projects/Server/Server.csproj +++ b/Projects/Server/Server.csproj @@ -37,10 +37,10 @@ - + - + diff --git a/Projects/UOContent.Tests/UOContent.Tests.csproj b/Projects/UOContent.Tests/UOContent.Tests.csproj index 77064faa1..dca5343eb 100644 --- a/Projects/UOContent.Tests/UOContent.Tests.csproj +++ b/Projects/UOContent.Tests/UOContent.Tests.csproj @@ -4,9 +4,9 @@ Debug;Release;Analyze - + - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/Projects/UOContent/Accounting/Security/AccountSecurity.cs b/Projects/UOContent/Accounting/Security/AccountSecurity.cs index 22860d370..355137ab7 100644 --- a/Projects/UOContent/Accounting/Security/AccountSecurity.cs +++ b/Projects/UOContent/Accounting/Security/AccountSecurity.cs @@ -1,6 +1,6 @@ /************************************************************************* * ModernUO * - * Copyright 2019-2023 - ModernUO Development Team * + * Copyright 2019-2025 - ModernUO Development Team * * Email: hi@modernuo.com * * File: AccountSecurity.cs * * * @@ -15,56 +15,55 @@ using System; -namespace Server.Accounting.Security -{ - public enum PasswordProtectionAlgorithm - { - // Obsolete algorithms from RunUO. These are not secure! - // They are included for password upgrades only. - None, - MD5, - SHA1, +namespace Server.Accounting.Security; - // Supported algorithms - SHA2, // ServUO compatibility - PBKDF2, - Argon2 // Recommended algorithm for real security. +public enum PasswordProtectionAlgorithm +{ + // Obsolete algorithms from RunUO. These are not secure! + // They are included for password upgrades only. + None, + MD5, + SHA1, + + // Supported algorithms + SHA2, // ServUO compatibility + PBKDF2, + Argon2 // Recommended algorithm for real security. +} + +public static class AccountSecurity +{ + public static PasswordProtectionAlgorithm CurrentAlgorithm { get; set; } + + public static IPasswordProtection CurrentPasswordProtection => GetPasswordProtection(CurrentAlgorithm); + + public static void Configure() + { + CurrentAlgorithm = + ServerConfiguration.GetOrUpdateSetting( + "accountSecurity.encryptionAlgorithm", + PasswordProtectionAlgorithm.Argon2 + ); + + if (CurrentAlgorithm < PasswordProtectionAlgorithm.SHA2) + { + throw new Exception($"Security: {CurrentAlgorithm} is obsolete and not secure. Do not use it."); + } } - public static class AccountSecurity + public static IPasswordProtection GetPasswordProtection(PasswordProtectionAlgorithm algorithm) { - public static PasswordProtectionAlgorithm CurrentAlgorithm { get; set; } - - public static IPasswordProtection CurrentPasswordProtection => GetPasswordProtection(CurrentAlgorithm); - - public static void Configure() + var passwordProtection = algorithm switch { - CurrentAlgorithm = - ServerConfiguration.GetOrUpdateSetting( - "accountSecurity.encryptionAlgorithm", - PasswordProtectionAlgorithm.Argon2 - ); + PasswordProtectionAlgorithm.MD5 => HashAlgorithmPasswordProtection.MD5Instance, + PasswordProtectionAlgorithm.SHA1 => HashAlgorithmPasswordProtection.SHA1Instance, + PasswordProtectionAlgorithm.SHA2 => HashAlgorithmPasswordProtection.SHA2Instance, + PasswordProtectionAlgorithm.PBKDF2 => PBKDF2PasswordProtection.Instance, + PasswordProtectionAlgorithm.Argon2 => Argon2PasswordProtection.Instance, + PasswordProtectionAlgorithm.None => throw new Exception("Do not use PasswordProtectionAlgorithm.None"), + _ => throw new Exception("No algorithm") + }; - if (CurrentAlgorithm < PasswordProtectionAlgorithm.SHA2) - { - throw new Exception($"Security: {CurrentAlgorithm} is obsolete and not secure. Do not use it."); - } - } - - public static IPasswordProtection GetPasswordProtection(PasswordProtectionAlgorithm algorithm) - { - var passwordProtection = algorithm switch - { - PasswordProtectionAlgorithm.MD5 => HashAlgorithmPasswordProtection.MD5Instance, - PasswordProtectionAlgorithm.SHA1 => HashAlgorithmPasswordProtection.SHA1Instance, - PasswordProtectionAlgorithm.SHA2 => HashAlgorithmPasswordProtection.SHA2Instance, - PasswordProtectionAlgorithm.PBKDF2 => PBKDF2PasswordProtection.Instance, - PasswordProtectionAlgorithm.Argon2 => Argon2PasswordProtection.Instance, - PasswordProtectionAlgorithm.None => throw new Exception("Do not use PasswordProtectionAlgorithm.None"), - _ => throw new Exception("No algorithm") - }; - - return passwordProtection; - } + return passwordProtection; } } diff --git a/Projects/UOContent/Accounting/Security/Argon2PasswordProtection.cs b/Projects/UOContent/Accounting/Security/Argon2PasswordProtection.cs index f49d2a040..6d3214a48 100644 --- a/Projects/UOContent/Accounting/Security/Argon2PasswordProtection.cs +++ b/Projects/UOContent/Accounting/Security/Argon2PasswordProtection.cs @@ -1,6 +1,6 @@ /************************************************************************* * ModernUO * - * Copyright 2019-2023 - ModernUO Development Team * + * Copyright 2019-2025 - ModernUO Development Team * * Email: hi@modernuo.com * * File: Argon2PasswordProtection.cs * * * @@ -15,18 +15,17 @@ using System.Security.Cryptography; -namespace Server.Accounting.Security +namespace Server.Accounting.Security; + +public class Argon2PasswordProtection : IPasswordProtection { - public class Argon2PasswordProtection : IPasswordProtection - { - public static IPasswordProtection Instance = new Argon2PasswordProtection(); + public static IPasswordProtection Instance = new Argon2PasswordProtection(); - private readonly Argon2PasswordHasher m_PasswordHasher = new(rng: BuiltInSecureRng.Generator); + private readonly Argon2PasswordHasher m_PasswordHasher = new(rng: RandomNumberGenerator.Create()); - public string EncryptPassword(string plainPassword) => - m_PasswordHasher.Hash(plainPassword); + public string EncryptPassword(string plainPassword) => + m_PasswordHasher.Hash(plainPassword); - public bool ValidatePassword(string encryptedPassword, string plainPassword) => - m_PasswordHasher.Verify(encryptedPassword, plainPassword); - } + public bool ValidatePassword(string encryptedPassword, string plainPassword) => + m_PasswordHasher.Verify(encryptedPassword, plainPassword); } diff --git a/Projects/UOContent/Accounting/Security/HashAlgorithmPasswordProtection.cs b/Projects/UOContent/Accounting/Security/HashAlgorithmPasswordProtection.cs index 978adbbf8..f50888675 100644 --- a/Projects/UOContent/Accounting/Security/HashAlgorithmPasswordProtection.cs +++ b/Projects/UOContent/Accounting/Security/HashAlgorithmPasswordProtection.cs @@ -1,6 +1,6 @@ /************************************************************************* * ModernUO * - * Copyright 2019-2023 - ModernUO Development Team * + * Copyright 2019-2025 - ModernUO Development Team * * Email: hi@modernuo.com * * File: HashAlgorithmPasswordProtection.cs * * * @@ -17,24 +17,23 @@ using System; using System.Security.Cryptography; using Server.Text; -namespace Server.Accounting.Security +namespace Server.Accounting.Security; + +public class HashAlgorithmPasswordProtection : IPasswordProtection { - public class HashAlgorithmPasswordProtection : IPasswordProtection + public static IPasswordProtection MD5Instance = new HashAlgorithmPasswordProtection(MD5.Create()); + public static IPasswordProtection SHA1Instance = new HashAlgorithmPasswordProtection(SHA1.Create()); + public static IPasswordProtection SHA2Instance = new HashAlgorithmPasswordProtection(SHA512.Create()); + private readonly HashAlgorithm _hashAlgorithm; + + public HashAlgorithmPasswordProtection(HashAlgorithm hashAlgorithm) => _hashAlgorithm = hashAlgorithm; + + public string EncryptPassword(string plainPassword) { - public static IPasswordProtection MD5Instance = new HashAlgorithmPasswordProtection(MD5.Create()); - public static IPasswordProtection SHA1Instance = new HashAlgorithmPasswordProtection(SHA1.Create()); - public static IPasswordProtection SHA2Instance = new HashAlgorithmPasswordProtection(SHA512.Create()); - private readonly HashAlgorithm _hashAlgorithm; - - public HashAlgorithmPasswordProtection(HashAlgorithm hashAlgorithm) => _hashAlgorithm = hashAlgorithm; - - public string EncryptPassword(string plainPassword) - { - byte[] bytes = plainPassword.AsSpan(0, Math.Min(256, plainPassword.Length)).GetBytesAscii(); - return _hashAlgorithm.ComputeHash(bytes).ToHexString(); - } - - public bool ValidatePassword(string encryptedPassword, string plainPassword) => - EncryptPassword(plainPassword) == encryptedPassword; + byte[] bytes = plainPassword.AsSpan(0, Math.Min(256, plainPassword.Length)).GetBytesAscii(); + return _hashAlgorithm.ComputeHash(bytes).ToHexString(); } + + public bool ValidatePassword(string encryptedPassword, string plainPassword) => + EncryptPassword(plainPassword) == encryptedPassword; } diff --git a/Projects/UOContent/Accounting/Security/PBKDF2PasswordProtection.cs b/Projects/UOContent/Accounting/Security/PBKDF2PasswordProtection.cs index e9fd8b05f..74f999a3f 100644 --- a/Projects/UOContent/Accounting/Security/PBKDF2PasswordProtection.cs +++ b/Projects/UOContent/Accounting/Security/PBKDF2PasswordProtection.cs @@ -1,6 +1,6 @@ /************************************************************************* * ModernUO * - * Copyright 2019-2023 - ModernUO Development Team * + * Copyright 2019-2025 - ModernUO Development Team * * Email: hi@modernuo.com * * File: PBKDF2PasswordProtection.cs * * * @@ -18,42 +18,43 @@ using System.Buffers.Binary; using System.Security.Cryptography; using Server.Text; -namespace Server.Accounting.Security +namespace Server.Accounting.Security; + +public class PBKDF2PasswordProtection : IPasswordProtection { - public class PBKDF2PasswordProtection : IPasswordProtection + private const ushort m_MinIterations = 1024; + private const ushort m_MaxIterations = 1536; + private const int m_SaltSize = 8; + private const int m_HashSize = 32; + private const int m_OutputSize = 2 + m_SaltSize + m_HashSize; + public static readonly IPasswordProtection Instance = new PBKDF2PasswordProtection(); + + public string EncryptPassword(string plainPassword) { - private const ushort m_MinIterations = 1024; - private const ushort m_MaxIterations = 1536; - private const int m_SaltSize = 8; - private const int m_HashSize = 32; - private const int m_OutputSize = 2 + m_SaltSize + m_HashSize; - public static readonly IPasswordProtection Instance = new PBKDF2PasswordProtection(); + Span output = stackalloc byte[m_OutputSize]; + var iterations = Utility.RandomMinMax(m_MinIterations, m_MaxIterations); + BinaryPrimitives.WriteUInt16LittleEndian(output[..2], (ushort)iterations); - public string EncryptPassword(string plainPassword) - { - Span output = stackalloc byte[m_OutputSize]; - var iterations = Utility.RandomMinMax(m_MinIterations, m_MaxIterations); - BinaryPrimitives.WriteUInt16LittleEndian(output[..2], (ushort)iterations); + var salt = output.Slice(2, m_SaltSize); + RandomNumberGenerator.Fill(salt); - var rfc2898 = new Rfc2898DeriveBytes(plainPassword, m_SaltSize, iterations, HashAlgorithmName.SHA256); - rfc2898.Salt.CopyTo(output.Slice(2, m_SaltSize)); - rfc2898.GetBytes(m_HashSize).CopyTo(output[(m_SaltSize + 2)..]); + var hash = output.Slice(2 + m_SaltSize, m_HashSize); + Rfc2898DeriveBytes.Pbkdf2(plainPassword, salt, hash, iterations, HashAlgorithmName.SHA256); - return output.ToHexString(); - } + return output.ToHexString(); + } - public bool ValidatePassword(string encryptedPassword, string plainPassword) - { - Span encryptedBytes = stackalloc byte[m_OutputSize]; - encryptedPassword.GetBytes(encryptedBytes); + public bool ValidatePassword(string encryptedPassword, string plainPassword) + { + Span encryptedBytes = stackalloc byte[m_OutputSize]; + encryptedPassword.GetBytes(encryptedBytes); - var iterations = BinaryPrimitives.ReadUInt16LittleEndian(encryptedBytes[..2]); - var salt = encryptedBytes.Slice(2, m_SaltSize); + var iterations = BinaryPrimitives.ReadUInt16LittleEndian(encryptedBytes[..2]); + var salt = encryptedBytes.Slice(2, m_SaltSize); - ReadOnlySpan hash = - new Rfc2898DeriveBytes(plainPassword, salt.ToArray(), iterations, HashAlgorithmName.SHA256).GetBytes(m_HashSize); + Span hash = stackalloc byte[m_HashSize]; + Rfc2898DeriveBytes.Pbkdf2(plainPassword, salt, hash, iterations, HashAlgorithmName.SHA256); - return hash.SequenceEqual(encryptedBytes[(m_SaltSize + 2)..]); - } + return hash.SequenceEqual(encryptedBytes[(m_SaltSize + 2)..]); } } diff --git a/Projects/UOContent/UOContent.csproj b/Projects/UOContent/UOContent.csproj index bea34e002..4ca0561f8 100644 --- a/Projects/UOContent/UOContent.csproj +++ b/Projects/UOContent/UOContent.csproj @@ -39,16 +39,16 @@ false - - + + - - + + diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 798907fc1..0a0fb6a25 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -14,7 +14,7 @@ jobs: steps: - task: UseDotNet@2 - displayName: 'Install .NET 9' + displayName: 'Install .NET' inputs: useGlobalJson: true - task: NuGetAuthenticate@1 diff --git a/global.json b/global.json index 733b653c1..6a288505a 100644 --- a/global.json +++ b/global.json @@ -1,6 +1,6 @@ { "sdk": { - "version": "9.0.100", + "version": "10.0.100", "rollForward": "latestMajor", "allowPrerelease": false } diff --git a/version.json b/version.json index d36382b04..de4574c38 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.15.1" + "version": "0.15.2" } From c308a3baadb7c20ffe254917141a33cf2e759ef5 Mon Sep 17 00:00:00 2001 From: Bohica <53943479+Bohicatv@users.noreply.github.com> Date: Wed, 12 Nov 2025 09:49:35 -0800 Subject: [PATCH 3/6] fix: Fixes timer leak in BaseCamp (#2259) --- Projects/UOContent/Multis/Camps/BaseCamp.cs | 29 +++++++++++++++++---- 1 file changed, 24 insertions(+), 5 deletions(-) diff --git a/Projects/UOContent/Multis/Camps/BaseCamp.cs b/Projects/UOContent/Multis/Camps/BaseCamp.cs index 3b9cece2a..e3d10dd0b 100644 --- a/Projects/UOContent/Multis/Camps/BaseCamp.cs +++ b/Projects/UOContent/Multis/Camps/BaseCamp.cs @@ -23,6 +23,7 @@ public abstract partial class BaseCamp : BaseMulti private TimeSpan _decayDelay; private Timer _decayTimer; + private Timer _initTimer; public BaseCamp(int multiID) : base(multiID) { @@ -31,7 +32,7 @@ public abstract partial class BaseCamp : BaseMulti _decayDelay = TimeSpan.FromMinutes(30.0); RefreshDecay(true); - Timer.StartTimer(CheckAddComponents); + _initTimer = Timer.DelayCall(TimeSpan.Zero, CheckAddComponents); } public virtual int EventRange => 10; @@ -50,6 +51,8 @@ public abstract partial class BaseCamp : BaseMulti public void CheckAddComponents() { + _initTimer = null; + if (Deleted) { return; @@ -138,16 +141,16 @@ public abstract partial class BaseCamp : BaseMulti for (var i = 0; i < _items.Count; ++i) { - _items[i].Delete(); + _items[i]?.Delete(); } for (var i = 0; i < _mobiles.Count; ++i) { var mob = _mobiles[i]; - if (mob.CantWalk || (mob as BaseCreature)?.IsPrisoner == false) + if (mob != null && (mob.CantWalk || (mob as BaseCreature)?.IsPrisoner == false)) { - _mobiles[i].Delete(); + mob.Delete(); } } @@ -156,6 +159,9 @@ public abstract partial class BaseCamp : BaseMulti _decayTimer?.Stop(); _decayTimer = null; + + _initTimer?.Stop(); + _initTimer = null; } private void Deserialize(IGenericReader reader, int version) @@ -168,7 +174,20 @@ public abstract partial class BaseCamp : BaseMulti [AfterDeserialization] private void AfterDeserialization() { - RefreshDecay(false); + var remaining = _decayTime - Core.Now; + + if (remaining > TimeSpan.Zero) + { + _decayDelay = remaining; + RefreshDecay(false); + } + else + { + Timer.DelayCall(TimeSpan.Zero, Delete); + return; + } + + _initTimer = Timer.DelayCall(TimeSpan.Zero, CheckAddComponents); } } From 5d920a25b1adaac85f7f3c40249d846815f6fbde Mon Sep 17 00:00:00 2001 From: Kamron Batman <3953314+kamronbatman@users.noreply.github.com> Date: Wed, 12 Nov 2025 23:03:14 -0800 Subject: [PATCH 4/6] chore: Updates .NET reference material to 10 (#2261) --- .github/workflows/build-test.yml | 13 ++-------- README.md | 44 ++++++++++++++++---------------- docs/installation.md | 6 ++--- 3 files changed, 27 insertions(+), 36 deletions(-) diff --git a/.github/workflows/build-test.yml b/.github/workflows/build-test.yml index 0a906a743..1c9ea98a9 100644 --- a/.github/workflows/build-test.yml +++ b/.github/workflows/build-test.yml @@ -54,20 +54,11 @@ jobs: - container: ubuntu:jammy name: Ubuntu 22 packageManager: apt - - container: ubuntu:focal - name: Ubuntu 20 - packageManager: apt - container: debian:bookworm name: Debian 12 packageManager: apt - - container: debian:bullseye - name: Debian 11 - packageManager: apt - - container: fedora:39 - name: Fedora 39 - packageManager: dnf - - container: fedora:40 - name: Fedora 40 + - container: fedora:42 + name: Fedora 42 packageManager: dnf - container: quay.io/centos/centos:stream9 name: CentOS 9 Stream diff --git a/README.md b/README.md index 4108f12c9..385219433 100644 --- a/README.md +++ b/README.md @@ -15,38 +15,38 @@ ModernUO [![Discord](https://img.shields.io/discord/751317910504603701?logo=disc ## Requirements #### Supported Operating Systems -[![Windows 11/2019/2022/2025](https://img.shields.io/badge/-server%202025-3c78d5?labelColor=222222&logo=data:image/svg%2bxml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHJvbGU9ImltZyIgdmlld0JveD0iMCAwIDI0IDI0Ij48dGl0bGU+V2luZG93czwvdGl0bGU+PHBhdGggZD0iTTAsMEgxMS4zNzdWMTEuMzcySDBaTTEyLjYyMywwSDI0VjExLjM3MkgxMi42MjNaTTAsMTIuNjIzSDExLjM3N1YyNEgwWm0xMi42MjMsMEgyNFYyNEgxMi42MjMiIGZpbGw9IiMzYzc4ZDUiLz48L3N2Zz4=)](https://www.microsoft.com/en-US/evalcenter/evaluate-windows-server-2022) -![MacOS 13+](https://img.shields.io/badge/-sequoia-222222?logo=apple&logoColor=white&labelColor=222222) +[![Windows 10/11/2012/2016/2019/2022/2025](https://img.shields.io/badge/-server%202025-3c78d5?labelColor=222222&logo=data:image/svg%2bxml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHJvbGU9ImltZyIgdmlld0JveD0iMCAwIDI0IDI0Ij48dGl0bGU+V2luZG93czwvdGl0bGU+PHBhdGggZD0iTTAsMEgxMS4zNzdWMTEuMzcySDBaTTEyLjYyMywwSDI0VjExLjM3MkgxMi42MjNaTTAsMTIuNjIzSDExLjM3N1YyNEgwWm0xMi42MjMsMEgyNFYyNEgxMi42MjMiIGZpbGw9IiMzYzc4ZDUiLz48L3N2Zz4=)](https://www.microsoft.com/en-US/evalcenter/evaluate-windows-server-2022) +![MacOS 14+](https://img.shields.io/badge/-sonoma-222222?logo=apple&logoColor=white&labelColor=222222) [![Debian 12+](https://img.shields.io/badge/-bookworm-A81D33?logo=debian&logoColor=A81D33&labelColor=222222)](https://www.debian.org/distrib/) [![Ubuntu 22+ LTS](https://img.shields.io/badge/-22LTS-E95420?logo=ubuntu&logoColor=E95420&labelColor=222222)](https://ubuntu.com/download/server)
-[![Alpine 3.19+](https://img.shields.io/badge/-3.21-0D597F?logo=alpinelinux&logoColor=0D597F&labelColor=222222)](https://alpinelinux.org/downloads/) -[![Fedora 40+](https://img.shields.io/badge/-40-51a2da?logo=fedora&logoColor=51a2da&labelColor=222222)](https://getfedora.org/en/server/download/) -[![RedHat 8+](https://img.shields.io/badge/-8-BE0000?logo=redhat&logoColor=BE0000&labelColor=222222)](https://access.redhat.com/downloads) -[![CentOS Stream 8+](https://img.shields.io/badge/-stream_8-262577?logo=centos&logoColor=white&labelColor=222222)](https://www.centos.org/download/) -[![openSUSE 15.5+](https://img.shields.io/badge/-15.5-73BA25?logo=openSUSE&logoColor=73BA25&labelColor=222222)](https://get.opensuse.org/) -[![SUSE Enterprise 12 SP2+](https://img.shields.io/badge/-12%20SP2-0C322C?logo=suse&logoColor=30BA78&labelColor=222222)](https://www.suse.com/download/sles/) -[![Linux Mint 17+](https://img.shields.io/badge/-20-87CF3E?logo=linux%20mint&logoColor=87CF3E&labelColor=222222)](https://linuxmint.com/download.php) +[![Alpine 3.22+](https://img.shields.io/badge/-3.22-0D597F?logo=alpinelinux&logoColor=0D597F&labelColor=222222)](https://alpinelinux.org/downloads/) +[![Fedora 42+](https://img.shields.io/badge/-42-51a2da?logo=fedora&logoColor=51a2da&labelColor=222222)](https://getfedora.org/en/server/download/) +[![RedHat 9+](https://img.shields.io/badge/-9-BE0000?logo=redhat&logoColor=BE0000&labelColor=222222)](https://access.redhat.com/downloads) +[![CentOS Stream 9+](https://img.shields.io/badge/-stream_9-262577?logo=centos&logoColor=white&labelColor=222222)](https://www.centos.org/download/) +[![openSUSE 15.6+](https://img.shields.io/badge/-15.6-73BA25?logo=openSUSE&logoColor=73BA25&labelColor=222222)](https://get.opensuse.org/) +[![SUSE Enterprise 15 SP6](https://img.shields.io/badge/-15%20SP6-0C322C?logo=suse&logoColor=30BA78&labelColor=222222)](https://www.suse.com/download/sles/) +[![Linux Mint 21+](https://img.shields.io/badge/-21-87CF3E?logo=linux%20mint&logoColor=87CF3E&labelColor=222222)](https://linuxmint.com/download.php) [![Arch](https://img.shields.io/badge/-Arch-1793D1?logo=archlinux&logoColor=1793D1&labelColor=222222)](https://archlinux.org/download/) #### Required Frameworks ##### All Operating Systems -[![.NET](https://img.shields.io/badge/-9.0.0-5C2D91?logo=.NET&logoColor=white&labelColor=222222)](https://dotnet.microsoft.com/download/dotnet/9.0) +[![.NET](https://img.shields.io/badge/-10.0.0-5C2D91?logo=.NET&logoColor=white&labelColor=222222)](https://dotnet.microsoft.com/download/dotnet/10.0) ##### Windows -[![VC++ Redistributable 17](https://img.shields.io/badge/-Redist%2017-00599C?logo=cplusplus&logoColor=white&labelColor=222222)](https://learn.microsoft.com/en-us/cpp/windows/latest-supported-vc-redist?view=msvc-170#visual-studio-2015-2017-2019-and-2022) +[![VC++ Redistributable v14](https://img.shields.io/badge/-Redist%20v14-00599C?logo=cplusplus&logoColor=white&labelColor=222222)](https://aka.ms/vc14/vc_redist.x64.exe) #### Development [![git](https://img.shields.io/badge/-git-F05032?logo=git&logoColor=F05032&labelColor=222222)](https://git-scm.com/downloads) -[![.NET](https://img.shields.io/badge/-%209.0.303%20SDK-5C2D91?logo=.NET&logoColor=white&labelColor=222222)](https://dotnet.microsoft.com/download/dotnet/9.0) +[![.NET](https://img.shields.io/badge/-%2010.0.100%20SDK-5C2D91?logo=.NET&logoColor=white&labelColor=222222)](https://dotnet.microsoft.com/download/dotnet/10.0) #### Supported IDEs

- Jetbrains Rider 2025+ + Jetbrains Rider 2025.3+ space VSCode space - Visual Studio 2022 v17.12+ + Visual Studio 2026

## Getting Started @@ -57,13 +57,13 @@ ModernUO [![Discord](https://img.shields.io/discord/751317910504603701?logo=disc ## Building/Publishing - Run `./publish.cmd [release|debug (default: release)] [os] [arch (default: x64)]` - - `os` - [Supported operating systems](https://github.com/dotnet/core/blob/main/release-notes/9.0/supported-os.md) - - `win` - Windows 10/11/2019/2022/2025 - - `osx` - MacOS 13/14/15 (Sequoia, Sonoma, Big Sur) - - `linux` - Linux + - `os` - [Supported operating systems](https://github.com/dotnet/core/blob/main/release-notes/10.0/supported-os.md) + - `win` - [Windows](https://learn.microsoft.com/en-us/dotnet/core/install/windows) + - `osx` - [MacOS](https://learn.microsoft.com/en-us/dotnet/core/install/macos) + - `linux` - [Linux](https://learn.microsoft.com/en-us/dotnet/core/install/linux) - `arch` - - `x64` - Intel 64-bit - - `arm64` - ARM 64-bit (Windows Arm64 not supported) + - `x64` - Intel/AMD 64-bit + - `arm64` - ARM 64-bit (Windows not supported) ## Linux Prerequisites ### Fedora, CentOS, RHEL, etc @@ -86,8 +86,8 @@ brew install icu4c libdeflate zstd argon2 ``` ## Running the Server -- Follow the [publish](https://github.com/modernuo/ModernUO#publishing-builds) instructions -- Run `ModernUO.exe` or `dotnet ModernUO.dll` from the `Distribution` directory on the +- Follow the [publish](https://github.com/modernuo/ModernUO#buildingpublishing) instructions +- Run `ModernUO.exe` or `dotnet ModernUO.dll` from the `Distribution` directory ## Troubleshooting / FAQ - See [FAQ](./FAQ.md) diff --git a/docs/installation.md b/docs/installation.md index ff6d90c8b..2fefc1d58 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -6,7 +6,7 @@ title: Installation === "Windows" ### Prerequisites - 1. Download and install the latest [.NET 8 SDK](https://dotnet.microsoft.com/download/dotnet/8.0) + 1. Download and install the latest [.NET 10 SDK](https://dotnet.microsoft.com/download/dotnet/10.0) 1. Download and install from [here](https://git-scm.com/download/win) !!! Tip @@ -22,7 +22,7 @@ title: Installation === "OSX" ### Prerequisites - 1. Download and install the latest [.NET 8 SDK](https://dotnet.microsoft.com/download/dotnet/8.0). + 1. Download and install the latest [.NET 10 SDK](https://dotnet.microsoft.com/download/dotnet/10.0). 1. Using _terminal_, install [homebrew](https://brew.sh) and git: ```bash /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install.sh)" @@ -37,7 +37,7 @@ title: Installation === "Linux" ### Prerequisites - 1. Download and install the latest [.NET 8 SDK](https://docs.microsoft.com/en-us/dotnet/core/install/linux). + 1. Download and install the latest [.NET 10 SDK](https://docs.microsoft.com/en-us/dotnet/core/install/linux). 1. Using _bash_, install git: ```bash sudo apt update && sudo apt install git From 68caaab92caa0bc0bb600ca7ba3189a5e8ad1de1 Mon Sep 17 00:00:00 2001 From: Kamron Batman <3953314+kamronbatman@users.noreply.github.com> Date: Wed, 12 Nov 2025 23:34:13 -0800 Subject: [PATCH 5/6] fix: Makes SpanWriter/SpanReader exceptions clearer (#2262) --- Projects/Server/Buffers/SpanReader.cs | 75 ++++++++++---------------- Projects/Server/Buffers/SpanWriter.cs | 76 ++++++++++----------------- 2 files changed, 55 insertions(+), 96 deletions(-) diff --git a/Projects/Server/Buffers/SpanReader.cs b/Projects/Server/Buffers/SpanReader.cs index 31c5fc51d..290ba5a94 100644 --- a/Projects/Server/Buffers/SpanReader.cs +++ b/Projects/Server/Buffers/SpanReader.cs @@ -1,6 +1,6 @@ /************************************************************************* * ModernUO * - * Copyright 2019-2023 - ModernUO Development Team * + * Copyright 2019-2025 - ModernUO Development Team * * Email: hi@modernuo.com * * File: SpanReader.cs * * * @@ -14,7 +14,6 @@ *************************************************************************/ using System.Buffers.Binary; -using System.Diagnostics; using System.IO; using System.Runtime.CompilerServices; using System.Text; @@ -45,7 +44,7 @@ public ref struct SpanReader { if (Position >= Length) { - throw new OutOfMemoryException(); + throw new EndOfStreamException("Cannot read past the end of the buffer."); } return _buffer[Position++]; @@ -62,7 +61,7 @@ public ref struct SpanReader { if (!BinaryPrimitives.TryReadInt16BigEndian(_buffer[Position..], out var value)) { - throw new OutOfMemoryException(); + throw new EndOfStreamException("Cannot read past the end of the buffer."); } Position += 2; @@ -74,7 +73,7 @@ public ref struct SpanReader { if (!BinaryPrimitives.TryReadInt16LittleEndian(_buffer[Position..], out var value)) { - throw new OutOfMemoryException(); + throw new EndOfStreamException("Cannot read past the end of the buffer."); } Position += 2; @@ -86,7 +85,7 @@ public ref struct SpanReader { if (!BinaryPrimitives.TryReadUInt16BigEndian(_buffer[Position..], out var value)) { - throw new OutOfMemoryException(); + throw new EndOfStreamException("Cannot read past the end of the buffer."); } Position += 2; @@ -98,7 +97,7 @@ public ref struct SpanReader { if (!BinaryPrimitives.TryReadUInt16LittleEndian(_buffer[Position..], out var value)) { - throw new OutOfMemoryException(); + throw new EndOfStreamException("Cannot read past the end of the buffer."); } Position += 2; @@ -110,7 +109,7 @@ public ref struct SpanReader { if (!BinaryPrimitives.TryReadInt32BigEndian(_buffer[Position..], out var value)) { - throw new OutOfMemoryException(); + throw new EndOfStreamException("Cannot read past the end of the buffer."); } Position += 4; @@ -122,7 +121,7 @@ public ref struct SpanReader { if (!BinaryPrimitives.TryReadUInt32BigEndian(_buffer[Position..], out var value)) { - throw new OutOfMemoryException(); + throw new EndOfStreamException("Cannot read past the end of the buffer."); } Position += 4; @@ -134,7 +133,7 @@ public ref struct SpanReader { if (!BinaryPrimitives.TryReadUInt32LittleEndian(_buffer[Position..], out var value)) { - throw new OutOfMemoryException(); + throw new EndOfStreamException("Cannot read past the end of the buffer."); } Position += 4; @@ -146,7 +145,7 @@ public ref struct SpanReader { if (!BinaryPrimitives.TryReadInt64BigEndian(_buffer[Position..], out var value)) { - throw new OutOfMemoryException(); + throw new EndOfStreamException("Cannot read past the end of the buffer."); } Position += 8; @@ -158,7 +157,7 @@ public ref struct SpanReader { if (!BinaryPrimitives.TryReadUInt64BigEndian(_buffer[Position..], out var value)) { - throw new OutOfMemoryException(); + throw new EndOfStreamException("Cannot read past the end of the buffer."); } Position += 8; @@ -173,9 +172,9 @@ public ref struct SpanReader return ""; } - int byteLength = encoding.GetByteLengthForEncoding(); + var byteLength = encoding.GetByteLengthForEncoding(); - bool isFixedLength = fixedLength > -1; + var isFixedLength = fixedLength > -1; var remaining = Remaining; int size; @@ -184,7 +183,7 @@ public ref struct SpanReader size = fixedLength * byteLength; if (size > Remaining) { - throw new OutOfMemoryException(); + throw new EndOfStreamException("Cannot read past the end of the buffer."); } } else @@ -255,42 +254,24 @@ public ref struct SpanReader [MethodImpl(MethodImplOptions.AggressiveInlining)] public int Seek(int offset, SeekOrigin origin) { - Debug.Assert( - origin != SeekOrigin.End || offset <= 0, - "Attempting to seek to a position beyond capacity using SeekOrigin.End" - ); - - Debug.Assert( - origin != SeekOrigin.End || offset >= -_buffer.Length, - "Attempting to seek to a negative position using SeekOrigin.End" - ); - - Debug.Assert( - origin != SeekOrigin.Begin || offset >= 0, - "Attempting to seek to a negative position using SeekOrigin.Begin" - ); - - Debug.Assert( - origin != SeekOrigin.Begin || offset <= _buffer.Length, - "Attempting to seek to a position beyond the capacity using SeekOrigin.Begin" - ); - - Debug.Assert( - origin != SeekOrigin.Current || Position + offset >= 0, - "Attempting to seek to a negative position using SeekOrigin.Current" - ); - - Debug.Assert( - origin != SeekOrigin.Current || Position + offset <= _buffer.Length, - "Attempting to seek to a position beyond the capacity using SeekOrigin.Current" - ); - - return Position = Math.Max(0, origin switch + var newPosition = origin switch { SeekOrigin.Current => Position + offset, SeekOrigin.End => _buffer.Length + offset, _ => offset // Begin - }); + }; + + if (newPosition < 0) + { + throw new ArgumentOutOfRangeException(nameof(offset), "Seek operation would result in a negative position."); + } + + if (newPosition > _buffer.Length) + { + throw new ArgumentOutOfRangeException(nameof(offset), $"Cannot seek to position {newPosition} beyond buffer length {_buffer.Length}."); + } + + return Position = newPosition; } [MethodImpl(MethodImplOptions.AggressiveInlining)] diff --git a/Projects/Server/Buffers/SpanWriter.cs b/Projects/Server/Buffers/SpanWriter.cs index 4ce423c5b..ccdaddd38 100644 --- a/Projects/Server/Buffers/SpanWriter.cs +++ b/Projects/Server/Buffers/SpanWriter.cs @@ -1,6 +1,6 @@ /************************************************************************* * ModernUO * - * Copyright 2019-2023 - ModernUO Development Team * + * Copyright 2019-2025 - ModernUO Development Team * * Email: hi@modernuo.com * * File: SpanWriter.cs * * * @@ -14,7 +14,6 @@ *************************************************************************/ using System.Buffers.Binary; -using System.Diagnostics; using System.IO; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; @@ -56,14 +55,14 @@ public ref struct SpanWriter public Span RawBuffer => _buffer; /** - * Converts the writer to a Span using a SpanOwner. - * If the buffer was stackalloc, it will be copied to a rented buffer. - * Otherwise the existing rented buffer is used. - * - * Note: - * Do not use the SpanWriter after calling this method. - * This method will effectively dispose of the SpanWriter and is therefore considered terminal. - */ + * Converts the writer to a Span using a SpanOwner. + * If the buffer was stackalloc, it will be copied to a rented buffer. + * Otherwise the existing rented buffer is used. + * + * Note: + * Do not use the SpanWriter after calling this method. + * This method will effectively dispose of the SpanWriter and is therefore considered terminal. + */ [MethodImpl(MethodImplOptions.AggressiveInlining)] public SpanOwner ToSpan() { @@ -117,11 +116,11 @@ public ref struct SpanWriter private void Grow(int additionalCapacity) { var newSize = Math.Max(BytesWritten + additionalCapacity, _buffer.Length * 2); - byte[] poolArray = STArrayPool.Shared.Rent(newSize); + var poolArray = STArrayPool.Shared.Rent(newSize); _buffer[..BytesWritten].CopyTo(poolArray); - byte[] toReturn = _arrayToReturnToPool; + var toReturn = _arrayToReturnToPool; _buffer = _arrayToReturnToPool = poolArray; if (toReturn != null) { @@ -136,7 +135,7 @@ public ref struct SpanWriter { if (!_resize) { - throw new OutOfMemoryException(); + throw new InvalidOperationException("Buffer is full and resizing is disabled."); } Grow(count); @@ -151,7 +150,7 @@ public ref struct SpanWriter { if (!_resize) { - throw new OutOfMemoryException(); + throw new InvalidOperationException("Buffer is full and resizing is disabled."); } Grow(capacity - BytesWritten); @@ -400,46 +399,25 @@ public ref struct SpanWriter [MethodImpl(MethodImplOptions.AggressiveInlining)] public int Seek(int offset, SeekOrigin origin) { - Debug.Assert( - origin != SeekOrigin.End || _resize || offset <= 0, - "Attempting to seek to a position beyond capacity using SeekOrigin.End without resize" - ); - - Debug.Assert( - origin != SeekOrigin.End || offset >= -_buffer.Length, - - "Attempting to seek to a negative position using SeekOrigin.End" - ); - - Debug.Assert( - origin != SeekOrigin.Begin || offset >= 0, - "Attempting to seek to a negative position using SeekOrigin.Begin" - ); - - Debug.Assert( - origin != SeekOrigin.Begin || _resize || offset <= _buffer.Length, - "Attempting to seek to a position beyond the capacity using SeekOrigin.Begin without resize" - ); - - Debug.Assert( - origin != SeekOrigin.Current || _position + offset >= 0, - "Attempting to seek to a negative position using SeekOrigin.Current" - ); - - Debug.Assert( - origin != SeekOrigin.Current || _resize || _position + offset <= _buffer.Length, - "Attempting to seek to a position beyond the capacity using SeekOrigin.Current without resize" - ); - - var newPosition = Math.Max(0, origin switch + var newPosition = origin switch { SeekOrigin.Current => _position + offset, SeekOrigin.End => BytesWritten + offset, _ => offset // Begin - }); + }; + + if (newPosition < 0) + { + throw new ArgumentOutOfRangeException(nameof(offset), "Seek operation would result in a negative position."); + } if (newPosition > _buffer.Length) { + if (!_resize) + { + throw new InvalidOperationException($"Cannot seek to position {newPosition} beyond buffer capacity {_buffer.Length} when resizing is disabled."); + } + Grow(newPosition - _buffer.Length + 1); } @@ -449,7 +427,7 @@ public ref struct SpanWriter [MethodImpl(MethodImplOptions.AggressiveInlining)] public void Dispose() { - byte[] toReturn = _arrayToReturnToPool; + var toReturn = _arrayToReturnToPool; this = default; // for safety, to avoid using pooled array if this instance is erroneously appended to again if (toReturn != null) { @@ -478,7 +456,7 @@ public ref struct SpanWriter [MethodImpl(MethodImplOptions.AggressiveInlining)] public void Dispose() { - byte[] toReturn = _arrayToReturnToPool; + var toReturn = _arrayToReturnToPool; this = default; if (_length > 0) { From 6f5b7f7a6b3e4265232663c7609acbe6301bbb42 Mon Sep 17 00:00:00 2001 From: Kamron Batman <3953314+kamronbatman@users.noreply.github.com> Date: Thu, 13 Nov 2025 00:00:19 -0800 Subject: [PATCH 6/6] fix: Fixes SpanWriter/SpanReader tests (#2263) --- .../Tests/Buffers/SpanReaderTests.cs | 597 +++++++++++++++++ .../Tests/Buffers/SpanWriterTests.cs | 618 ++++++++++++++++-- Projects/Server/Buffers/SpanReader.cs | 2 +- 3 files changed, 1171 insertions(+), 46 deletions(-) create mode 100644 Projects/Server.Tests/Tests/Buffers/SpanReaderTests.cs diff --git a/Projects/Server.Tests/Tests/Buffers/SpanReaderTests.cs b/Projects/Server.Tests/Tests/Buffers/SpanReaderTests.cs new file mode 100644 index 000000000..9a68576c8 --- /dev/null +++ b/Projects/Server.Tests/Tests/Buffers/SpanReaderTests.cs @@ -0,0 +1,597 @@ +using System; +using System.Buffers; +using System.IO; +using Xunit; + +namespace Server.Tests; + +public class SpanReaderTests +{ + [Fact] + public void TestReadByte() + { + ReadOnlySpan buffer = [0x12, 0x34]; + var reader = new SpanReader(buffer); + + Assert.Equal(0x12, reader.ReadByte()); + Assert.Equal(0x34, reader.ReadByte()); + Assert.Equal(2, reader.Position); + } + + [Fact] + public void TestReadByteAtEnd() + { + Assert.Throws( + () => + { + ReadOnlySpan buffer = [0x12]; + var reader = new SpanReader(buffer); + reader.ReadByte(); + reader.ReadByte(); + } + ); + } + + [Fact] + public void TestReadBoolean() + { + ReadOnlySpan buffer = [0, 1, 2, 255]; + var reader = new SpanReader(buffer); + + Assert.False(reader.ReadBoolean()); + Assert.True(reader.ReadBoolean()); + Assert.True(reader.ReadBoolean()); + Assert.True(reader.ReadBoolean()); + Assert.Equal(4, reader.Position); + } + + [Fact] + public void TestReadSByte() + { + ReadOnlySpan buffer = [0xFF, 0x7F]; + var reader = new SpanReader(buffer); + + Assert.Equal(-1, reader.ReadSByte()); + Assert.Equal(127, reader.ReadSByte()); + Assert.Equal(2, reader.Position); + } + + [Fact] + public void TestReadInt16BigEndian() + { + ReadOnlySpan buffer = [0x12, 0x34]; + var reader = new SpanReader(buffer); + + Assert.Equal(0x1234, reader.ReadInt16()); + Assert.Equal(2, reader.Position); + } + + [Fact] + public void TestReadInt16LittleEndian() + { + ReadOnlySpan buffer = [0x34, 0x12]; + var reader = new SpanReader(buffer); + + Assert.Equal(0x1234, reader.ReadInt16LE()); + Assert.Equal(2, reader.Position); + } + + [Fact] + public void TestReadInt16AtEnd() + { + Assert.Throws( + () => + { + ReadOnlySpan buffer = [0x12]; + var reader = new SpanReader(buffer); + reader.ReadInt16(); + } + ); + } + + [Fact] + public void TestReadUInt16BigEndian() + { + ReadOnlySpan buffer = [0x12, 0x34]; + var reader = new SpanReader(buffer); + + Assert.Equal((ushort)0x1234, reader.ReadUInt16()); + Assert.Equal(2, reader.Position); + } + + [Fact] + public void TestReadUInt16LittleEndian() + { + ReadOnlySpan buffer = [0x34, 0x12]; + var reader = new SpanReader(buffer); + + Assert.Equal((ushort)0x1234, reader.ReadUInt16LE()); + Assert.Equal(2, reader.Position); + } + + [Fact] + public void TestReadInt32BigEndian() + { + ReadOnlySpan buffer = [0x12, 0x34, 0x56, 0x78]; + var reader = new SpanReader(buffer); + + Assert.Equal(0x12345678, reader.ReadInt32()); + Assert.Equal(4, reader.Position); + } + + [Fact] + public void TestReadInt32AtEnd() + { + Assert.Throws( + () => + { + ReadOnlySpan buffer = [0x12, 0x34, 0x56]; + var reader = new SpanReader(buffer); + reader.ReadInt32(); + } + ); + } + + [Fact] + public void TestReadUInt32BigEndian() + { + ReadOnlySpan buffer = [0x12, 0x34, 0x56, 0x78]; + var reader = new SpanReader(buffer); + + Assert.Equal(0x12345678u, reader.ReadUInt32()); + Assert.Equal(4, reader.Position); + } + + [Fact] + public void TestReadUInt32LittleEndian() + { + ReadOnlySpan buffer = [0x78, 0x56, 0x34, 0x12]; + var reader = new SpanReader(buffer); + + Assert.Equal(0x12345678u, reader.ReadUInt32LE()); + Assert.Equal(4, reader.Position); + } + + [Fact] + public void TestReadInt64BigEndian() + { + ReadOnlySpan buffer = [0x12, 0x34, 0x56, 0x78, 0x9A, 0xBC, 0xDE, 0xF0]; + var reader = new SpanReader(buffer); + + Assert.Equal(0x123456789ABCDEF0L, reader.ReadInt64()); + Assert.Equal(8, reader.Position); + } + + [Fact] + public void TestReadInt64AtEnd() + { + Assert.Throws( + () => + { + ReadOnlySpan buffer = [0x12, 0x34, 0x56, 0x78, 0x9A, 0xBC, 0xDE]; + var reader = new SpanReader(buffer); + reader.ReadInt64(); + } + ); + } + + [Fact] + public void TestReadUInt64BigEndian() + { + ReadOnlySpan buffer = [0x12, 0x34, 0x56, 0x78, 0x9A, 0xBC, 0xDE, 0xF0]; + var reader = new SpanReader(buffer); + + Assert.Equal(0x123456789ABCDEF0UL, reader.ReadUInt64()); + Assert.Equal(8, reader.Position); + } + + [Fact] + public void TestReadAsciiString() + { + ReadOnlySpan buffer = "Hello"u8; + var reader = new SpanReader(buffer); + + var result = reader.ReadAscii(); + + Assert.Equal("Hello", result); + Assert.Equal(5, reader.Position); + } + + [Fact] + public void TestReadAsciiStringWithNull() + { + ReadOnlySpan buffer = "Hel\0o"u8; + var reader = new SpanReader(buffer); + + var result = reader.ReadAscii(); + + Assert.Equal("Hel", result); + Assert.Equal(4, reader.Position); + } + + [Fact] + public void TestReadAsciiStringFixedLength() + { + ReadOnlySpan buffer = "Hello\0\0\0\0\0"u8; + var reader = new SpanReader(buffer); + + var result = reader.ReadAscii(10); + + Assert.Equal("Hello", result); + Assert.Equal(10, reader.Position); + } + + [Fact] + public void TestReadAsciiStringFixedLengthTooLarge() + { + Assert.Throws( + () => + { + ReadOnlySpan buffer = "Hel"u8; + var reader = new SpanReader(buffer); + reader.ReadAscii(10); + } + ); + } + + [Fact] + public void TestReadAsciiSafe() + { + ReadOnlySpan buffer = [(byte)'H', (byte)'e', 0xFF, (byte)'l', (byte)'o']; + var reader = new SpanReader(buffer); + + var result = reader.ReadAsciiSafe(); + + Assert.Equal("He?lo", result); + Assert.Equal(5, reader.Position); + } + + [Fact] + public void TestReadUTF8String() + { + ReadOnlySpan buffer = "Hello"u8; + var reader = new SpanReader(buffer); + + var result = reader.ReadUTF8(); + + Assert.Equal("Hello", result); + Assert.Equal(5, reader.Position); + } + + [Fact] + public void TestReadUTF8StringWithNull() + { + ReadOnlySpan buffer = "Hi\0Bye"u8; + var reader = new SpanReader(buffer); + + var result = reader.ReadUTF8(); + + Assert.Equal("Hi", result); + Assert.Equal(3, reader.Position); + } + + [Fact] + public void TestReadLittleUniString() + { + ReadOnlySpan buffer = "H\0i\0"u8; + var reader = new SpanReader(buffer); + + var result = reader.ReadLittleUni(); + + Assert.Equal("Hi", result); + Assert.Equal(4, reader.Position); + } + + [Fact] + public void TestReadLittleUniStringWithNull() + { + ReadOnlySpan buffer = "H\0i\0\0\0X\0"u8; + var reader = new SpanReader(buffer); + + var result = reader.ReadLittleUni(); + + Assert.Equal("Hi", result); + Assert.Equal(5, reader.Position); + } + + [Fact] + public void TestReadLittleUniStringFixedLength() + { + ReadOnlySpan buffer = "H\0i\0\0\0\0\0"u8; + var reader = new SpanReader(buffer); + + var result = reader.ReadLittleUni(4); + + Assert.Equal("Hi", result); + Assert.Equal(8, reader.Position); + } + + [Fact] + public void TestReadLittleUniSafe() + { + ReadOnlySpan buffer = [(byte)'H', 0, 0xFF, 0xD8, (byte)'i', 0]; + var reader = new SpanReader(buffer); + + var result = reader.ReadLittleUniSafe(); + + Assert.Equal("H\uFFFDi", result); + Assert.Equal(6, reader.Position); + } + + [Fact] + public void TestReadBigUniString() + { + ReadOnlySpan buffer = "\0H\0i"u8; + var reader = new SpanReader(buffer); + + var result = reader.ReadBigUni(); + + Assert.Equal("Hi", result); + Assert.Equal(4, reader.Position); + } + + [Fact] + public void TestReadBigUniStringWithNull() + { + ReadOnlySpan buffer = "\0H\0i\0\0\0X"u8; + var reader = new SpanReader(buffer); + + var result = reader.ReadBigUni(); + + Assert.Equal("Hi", result); + Assert.Equal(5, reader.Position); + } + + [Fact] + public void TestReadBigUniStringFixedLength() + { + ReadOnlySpan buffer = "\0H\0i\0\0\0\0"u8; + var reader = new SpanReader(buffer); + + var result = reader.ReadBigUni(4); + + Assert.Equal("Hi", result); + Assert.Equal(8, reader.Position); + } + + [Fact] + public void TestReadBigUniSafe() + { + ReadOnlySpan buffer = [0, (byte)'H', 0xD8, 0xFF, 0, (byte)'i']; + var reader = new SpanReader(buffer); + + var result = reader.ReadBigUniSafe(); + + Assert.Equal("H\uFFFDi", result); + Assert.Equal(6, reader.Position); + } + + [Fact] + public void TestSeekBegin() + { + ReadOnlySpan buffer = [0x01, 0x02, 0x03, 0x04, 0x05]; + var reader = new SpanReader(buffer); + + reader.ReadByte(); + reader.ReadByte(); + + var pos = reader.Seek(0, SeekOrigin.Begin); + + Assert.Equal(0, pos); + Assert.Equal(0, reader.Position); + Assert.Equal(0x01, reader.ReadByte()); + } + + [Fact] + public void TestSeekCurrent() + { + ReadOnlySpan buffer = [0x01, 0x02, 0x03, 0x04, 0x05]; + var reader = new SpanReader(buffer); + + reader.ReadByte(); + var pos = reader.Seek(2, SeekOrigin.Current); + + Assert.Equal(3, pos); + Assert.Equal(3, reader.Position); + Assert.Equal(0x04, reader.ReadByte()); + } + + [Fact] + public void TestSeekEnd() + { + ReadOnlySpan buffer = [0x01, 0x02, 0x03, 0x04, 0x05]; + var reader = new SpanReader(buffer); + + var pos = reader.Seek(-2, SeekOrigin.End); + + Assert.Equal(3, pos); + Assert.Equal(3, reader.Position); + Assert.Equal(0x04, reader.ReadByte()); + } + + [Fact] + public void TestSeekNegativeThrows() + { + Assert.Throws( + () => + { + ReadOnlySpan buffer = [0x01, 0x02, 0x03]; + var reader = new SpanReader(buffer); + reader.Seek(-1, SeekOrigin.Begin); + } + ); + } + + [Fact] + public void TestSeekBeyondEndThrows() + { + Assert.Throws( + () => + { + ReadOnlySpan buffer = [0x01, 0x02, 0x03]; + var reader = new SpanReader(buffer); + reader.Seek(10, SeekOrigin.Begin); + } + ); + } + + [Fact] + public void TestRead() + { + ReadOnlySpan buffer = [0x01, 0x02, 0x03, 0x04, 0x05]; + var reader = new SpanReader(buffer); + + Span dest = stackalloc byte[3]; + var bytesRead = reader.Read(dest); + + Assert.Equal(3, bytesRead); + Assert.Equal(3, reader.Position); + AssertThat.Equal(dest, [0x01, 0x02, 0x03]); + } + + [Fact] + public void TestReadPartial() + { + ReadOnlySpan buffer = [0x01, 0x02, 0x03]; + var reader = new SpanReader(buffer); + + Span dest = stackalloc byte[5]; + var bytesRead = reader.Read(dest); + + Assert.Equal(3, bytesRead); + Assert.Equal(3, reader.Position); + AssertThat.Equal(dest[..3], [0x01, 0x02, 0x03]); + } + + [Fact] + public void TestReadEmpty() + { + ReadOnlySpan buffer = [0x01, 0x02, 0x03]; + var reader = new SpanReader(buffer); + + Span dest = []; + var bytesRead = reader.Read(dest); + + Assert.Equal(0, bytesRead); + Assert.Equal(0, reader.Position); + } + + [Fact] + public void TestReadAtEnd() + { + ReadOnlySpan buffer = [0x01, 0x02, 0x03]; + var reader = new SpanReader(buffer); + + reader.Seek(3, SeekOrigin.Begin); + + Span dest = stackalloc byte[5]; + var bytesRead = reader.Read(dest); + + Assert.Equal(0, bytesRead); + Assert.Equal(3, reader.Position); + } + + [Fact] + public void TestLength() + { + ReadOnlySpan buffer = [0x01, 0x02, 0x03, 0x04, 0x05]; + var reader = new SpanReader(buffer); + + Assert.Equal(5, reader.Length); + } + + [Fact] + public void TestPosition() + { + ReadOnlySpan buffer = [0x01, 0x02, 0x03]; + var reader = new SpanReader(buffer); + + Assert.Equal(0, reader.Position); + + reader.ReadByte(); + Assert.Equal(1, reader.Position); + + reader.ReadUInt16(); + Assert.Equal(3, reader.Position); + } + + [Fact] + public void TestRemaining() + { + ReadOnlySpan buffer = [0x01, 0x02, 0x03, 0x04, 0x05]; + var reader = new SpanReader(buffer); + + Assert.Equal(5, reader.Remaining); + + reader.ReadByte(); + Assert.Equal(4, reader.Remaining); + + reader.ReadUInt16(); + Assert.Equal(2, reader.Remaining); + + reader.ReadUInt16(); + Assert.Equal(0, reader.Remaining); + } + + [Fact] + public void TestBuffer() + { + ReadOnlySpan buffer = [0x01, 0x02, 0x03]; + var reader = new SpanReader(buffer); + + var bufferProperty = reader.Buffer; + + Assert.Equal(3, bufferProperty.Length); + AssertThat.Equal(bufferProperty, buffer); + } + + [Fact] + public void TestReadStringEmptyFixedLength() + { + ReadOnlySpan buffer = [0x01, 0x02, 0x03]; + var reader = new SpanReader(buffer); + + var result = reader.ReadAscii(0); + + Assert.Equal("", result); + Assert.Equal(0, reader.Position); + } + + [Fact] + public void TestReadMultipleStringsWithNullTerminators() + { + ReadOnlySpan buffer = "AB\0CD\0EF"u8; + var reader = new SpanReader(buffer); + + Assert.Equal("AB", reader.ReadAscii()); + Assert.Equal("CD", reader.ReadAscii()); + Assert.Equal("EF", reader.ReadAscii()); + Assert.Equal(8, reader.Position); + } + + [Fact] + public void TestReadLittleUniOddByteCount() + { + // If buffer has odd number of bytes, the last byte should be ignored + ReadOnlySpan buffer = [(byte)'H', 0, (byte)'i', 0, 0xFF]; + var reader = new SpanReader(buffer); + + var result = reader.ReadLittleUni(); + + Assert.Equal("Hi", result); + Assert.Equal(4, reader.Position); + } + + [Fact] + public void TestReadBigUniOddByteCount() + { + // If buffer has odd number of bytes, the last byte should be ignored + ReadOnlySpan buffer = [0, (byte)'H', 0, (byte)'i', 0xFF]; + var reader = new SpanReader(buffer); + + var result = reader.ReadBigUni(); + + Assert.Equal("Hi", result); + Assert.Equal(4, reader.Position); + } +} diff --git a/Projects/Server.Tests/Tests/Buffers/SpanWriterTests.cs b/Projects/Server.Tests/Tests/Buffers/SpanWriterTests.cs index 0aeffe574..a0e7ed18c 100644 --- a/Projects/Server.Tests/Tests/Buffers/SpanWriterTests.cs +++ b/Projects/Server.Tests/Tests/Buffers/SpanWriterTests.cs @@ -1,63 +1,591 @@ using System; using System.Buffers; +using System.IO; using Xunit; -namespace Server.Tests +namespace Server.Tests; + +public class SpanWriterTests { - public class SpanWriterTests + [Fact] + public unsafe void TestSpanWriterResizes() { - [Fact] - public unsafe void TestSpanWriterResizes() - { - Span smallStack = stackalloc byte[8]; - using var writer = new SpanWriter(smallStack, true); - writer.Write(0x1024L); - writer.Write(0x1024L); + Span smallStack = stackalloc byte[8]; + var writer = new SpanWriter(smallStack, true); + writer.Write(0x1024L); + writer.Write(0x1024L); - var span = writer.RawBuffer; - fixed (byte* spanPtr = span) + var span = writer.RawBuffer; + fixed (byte* spanPtr = span) + { + fixed (byte* stackPtr = smallStack) { - fixed (byte* stackPtr = smallStack) - { - Assert.True(spanPtr != stackPtr); - } + Assert.True(spanPtr != stackPtr); } - - Assert.True(span.Length > smallStack.Length); } - [Fact] - public unsafe void TestSpanWriterOnlyStackAlloc() - { - Span smallStack = stackalloc byte[8]; - using var writer = new SpanWriter(smallStack, true); - writer.Write(0x1024L); + Assert.True(span.Length > smallStack.Length); + writer.Dispose(); + } - var span = writer.RawBuffer; - fixed (byte* spanPtr = span) + [Fact] + public unsafe void TestSpanWriterOnlyStackAlloc() + { + Span smallStack = stackalloc byte[8]; + var writer = new SpanWriter(smallStack, true); + writer.Write(0x1024L); + + var span = writer.RawBuffer; + fixed (byte* spanPtr = span) + { + fixed (byte* stackPtr = smallStack) { - fixed (byte* stackPtr = smallStack) - { - Assert.True(spanPtr == stackPtr); - } + Assert.True(spanPtr == stackPtr); } - - Assert.True(span.Length == smallStack.Length); - AssertThat.Equal(smallStack, stackalloc byte[] { 0, 0, 0, 0, 0, 0, 0x10, 0x24 }); } - [Fact] - public void TestSpanWriterNoResizeThrows() - { - Assert.Throws( - () => - { - Span smallStack = stackalloc byte[8]; - using var writer = new SpanWriter(smallStack); - writer.Write(0x1024L); - writer.Write(0x1024L); - } - ); - } + Assert.Equal(8, span.Length); + AssertThat.Equal(smallStack, [0, 0, 0, 0, 0, 0, 0x10, 0x24]); + } + + [Fact] + public void TestSpanWriterNoResizeThrows() + { + Assert.Throws( + () => + { + Span smallStack = stackalloc byte[8]; + var writer = new SpanWriter(smallStack); + writer.Write(0x1024L); + writer.Write(0x1024L); + } + ); + } + + [Fact] + public void TestWriteBool() + { + Span buffer = stackalloc byte[2]; + var writer = new SpanWriter(buffer); + + writer.Write(true); + writer.Write(false); + + Assert.Equal(2, writer.Position); + AssertThat.Equal(writer.Span, [1, 0]); + } + + [Fact] + public void TestWriteByte() + { + Span buffer = stackalloc byte[2]; + var writer = new SpanWriter(buffer); + + writer.Write((byte)0x12); + writer.Write((byte)0x34); + + Assert.Equal(2, writer.Position); + AssertThat.Equal(writer.Span, [0x12, 0x34]); + } + + [Fact] + public void TestWriteSByte() + { + Span buffer = stackalloc byte[2]; + var writer = new SpanWriter(buffer); + + writer.Write((sbyte)-1); + writer.Write((sbyte)127); + + Assert.Equal(2, writer.Position); + AssertThat.Equal(writer.Span, [0xFF, 0x7F]); + } + + [Fact] + public void TestWriteInt16BigEndian() + { + Span buffer = stackalloc byte[2]; + var writer = new SpanWriter(buffer); + + writer.Write((short)0x1234); + + Assert.Equal(2, writer.Position); + AssertThat.Equal(writer.Span, [0x12, 0x34]); + } + + [Fact] + public void TestWriteInt16LittleEndian() + { + Span buffer = stackalloc byte[2]; + var writer = new SpanWriter(buffer); + + writer.WriteLE((short)0x1234); + + Assert.Equal(2, writer.Position); + AssertThat.Equal(writer.Span, [0x34, 0x12]); + } + + [Fact] + public void TestWriteUInt16BigEndian() + { + Span buffer = stackalloc byte[2]; + var writer = new SpanWriter(buffer); + + writer.Write((ushort)0x1234); + + Assert.Equal(2, writer.Position); + AssertThat.Equal(writer.Span, [0x12, 0x34]); + } + + [Fact] + public void TestWriteUInt16LittleEndian() + { + Span buffer = stackalloc byte[2]; + var writer = new SpanWriter(buffer); + + writer.WriteLE((ushort)0x1234); + + Assert.Equal(2, writer.Position); + AssertThat.Equal(writer.Span, [0x34, 0x12]); + } + + [Fact] + public void TestWriteInt32BigEndian() + { + Span buffer = stackalloc byte[4]; + var writer = new SpanWriter(buffer); + + writer.Write(0x12345678); + + Assert.Equal(4, writer.Position); + AssertThat.Equal(writer.Span, [0x12, 0x34, 0x56, 0x78]); + } + + [Fact] + public void TestWriteInt32LittleEndian() + { + Span buffer = stackalloc byte[4]; + var writer = new SpanWriter(buffer); + + writer.WriteLE(0x12345678); + + Assert.Equal(4, writer.Position); + AssertThat.Equal(writer.Span, [0x78, 0x56, 0x34, 0x12]); + } + + [Fact] + public void TestWriteUInt32BigEndian() + { + Span buffer = stackalloc byte[4]; + var writer = new SpanWriter(buffer); + + writer.Write(0x12345678u); + + Assert.Equal(4, writer.Position); + AssertThat.Equal(writer.Span, [0x12, 0x34, 0x56, 0x78]); + } + + [Fact] + public void TestWriteUInt32LittleEndian() + { + Span buffer = stackalloc byte[4]; + var writer = new SpanWriter(buffer); + + writer.WriteLE(0x12345678u); + + Assert.Equal(4, writer.Position); + AssertThat.Equal(writer.Span, [0x78, 0x56, 0x34, 0x12]); + } + + [Fact] + public void TestWriteInt64BigEndian() + { + Span buffer = stackalloc byte[8]; + var writer = new SpanWriter(buffer); + + writer.Write(0x123456789ABCDEF0L); + + Assert.Equal(8, writer.Position); + AssertThat.Equal(writer.Span, [0x12, 0x34, 0x56, 0x78, 0x9A, 0xBC, 0xDE, 0xF0]); + } + + [Fact] + public void TestWriteUInt64BigEndian() + { + Span buffer = stackalloc byte[8]; + var writer = new SpanWriter(buffer); + + writer.Write(0x123456789ABCDEF0UL); + + Assert.Equal(8, writer.Position); + AssertThat.Equal(writer.Span, [0x12, 0x34, 0x56, 0x78, 0x9A, 0xBC, 0xDE, 0xF0]); + } + + [Fact] + public void TestWriteSpan() + { + Span buffer = stackalloc byte[6]; + var writer = new SpanWriter(buffer); + + ReadOnlySpan data = [0x01, 0x02, 0x03]; + writer.Write(data); + writer.Write(data); + + Assert.Equal(6, writer.Position); + AssertThat.Equal(writer.Span, [0x01, 0x02, 0x03, 0x01, 0x02, 0x03]); + } + + [Fact] + public void TestWriteAsciiChar() + { + Span buffer = stackalloc byte[2]; + var writer = new SpanWriter(buffer); + + writer.WriteAscii('A'); + writer.WriteAscii('B'); + + Assert.Equal(2, writer.Position); + AssertThat.Equal(writer.Span, "AB"u8); + } + + [Fact] + public void TestWriteAsciiString() + { + Span buffer = stackalloc byte[5]; + var writer = new SpanWriter(buffer); + + writer.WriteAscii("Hello"); + + Assert.Equal(5, writer.Position); + AssertThat.Equal(writer.Span, "Hello"u8); + } + + [Fact] + public void TestWriteAsciiStringNull() + { + Span buffer = stackalloc byte[6]; + var writer = new SpanWriter(buffer); + + writer.WriteAsciiNull("Hello"); + + Assert.Equal(6, writer.Position); + AssertThat.Equal(writer.Span, "Hello\0"u8); + } + + [Fact] + public void TestWriteAsciiStringFixedLength() + { + Span buffer = stackalloc byte[10]; + var writer = new SpanWriter(buffer); + + writer.WriteAscii("Hello", 10); + + Assert.Equal(10, writer.Position); + AssertThat.Equal(writer.Span, "Hello\0\0\0\0\0"u8); + } + + [Fact] + public void TestWriteUTF8String() + { + Span buffer = stackalloc byte[5]; + var writer = new SpanWriter(buffer); + + writer.WriteUTF8("Hello"); + + Assert.Equal(5, writer.Position); + AssertThat.Equal(writer.Span, "Hello"u8); + } + + [Fact] + public void TestWriteUTF8StringNull() + { + Span buffer = stackalloc byte[6]; + var writer = new SpanWriter(buffer); + + writer.WriteUTF8Null("Hello"); + + Assert.Equal(6, writer.Position); + AssertThat.Equal(writer.Span, "Hello\0"u8); + } + + [Fact] + public void TestWriteLittleUniString() + { + Span buffer = stackalloc byte[10]; + var writer = new SpanWriter(buffer); + + writer.WriteLittleUni("Hello"); + + Assert.Equal(10, writer.Position); + AssertThat.Equal(writer.Span, "H\0e\0l\0l\0o\0"u8); + } + + [Fact] + public void TestWriteLittleUniStringNull() + { + Span buffer = stackalloc byte[12]; + var writer = new SpanWriter(buffer); + + writer.WriteLittleUniNull("Hello"); + + Assert.Equal(12, writer.Position); + AssertThat.Equal(writer.Span, "H\0e\0l\0l\0o\0\0\0"u8); + } + + [Fact] + public void TestWriteBigUniString() + { + Span buffer = stackalloc byte[10]; + var writer = new SpanWriter(buffer); + + writer.WriteBigUni("Hello"); + + Assert.Equal(10, writer.Position); + AssertThat.Equal(writer.Span, "\0H\0e\0l\0l\0o"u8); + } + + [Fact] + public void TestWriteBigUniStringNull() + { + Span buffer = stackalloc byte[12]; + var writer = new SpanWriter(buffer); + + writer.WriteBigUniNull("Hello"); + + Assert.Equal(12, writer.Position); + AssertThat.Equal(writer.Span, "\0H\0e\0l\0l\0o\0\0"u8); + } + + [Fact] + public void TestClear() + { + Span buffer = stackalloc byte[10]; + buffer.Fill(0xFF); + var writer = new SpanWriter(buffer); + + writer.Write((byte)0x01); + writer.Clear(5); + writer.Write((byte)0x02); + + Assert.Equal(7, writer.Position); + AssertThat.Equal(writer.Span, [0x01, 0, 0, 0, 0, 0, 0x02]); + } + + [Fact] + public void TestSeekBegin() + { + Span buffer = stackalloc byte[10]; + var writer = new SpanWriter(buffer); + + writer.Write((byte)0x01); + writer.Write((byte)0x02); + writer.Write((byte)0x03); + + var pos = writer.Seek(1, SeekOrigin.Begin); + + Assert.Equal(1, pos); + Assert.Equal(1, writer.Position); + Assert.Equal(3, writer.BytesWritten); + } + + [Fact] + public void TestSeekCurrent() + { + Span buffer = stackalloc byte[10]; + var writer = new SpanWriter(buffer); + + writer.Write((byte)0x01); + writer.Write((byte)0x02); + + var pos = writer.Seek(2, SeekOrigin.Current); + + Assert.Equal(4, pos); + Assert.Equal(4, writer.Position); + } + + [Fact] + public void TestSeekEnd() + { + Span buffer = stackalloc byte[10]; + var writer = new SpanWriter(buffer); + + writer.Write((byte)0x01); + writer.Write((byte)0x02); + writer.Write((byte)0x03); + + var pos = writer.Seek(-1, SeekOrigin.End); + + Assert.Equal(2, pos); + Assert.Equal(2, writer.Position); + } + + [Fact] + public void TestSeekNegativeThrows() + { + Assert.Throws( + () => + { + Span buffer = stackalloc byte[10]; + var writer = new SpanWriter(buffer); + writer.Seek(-1, SeekOrigin.Begin); + } + ); + } + + [Fact] + public void TestSeekBeyondCapacityNoResizeThrows() + { + Assert.Throws( + () => + { + Span buffer = stackalloc byte[10]; + var writer = new SpanWriter(buffer); + writer.Seek(20, SeekOrigin.Begin); + } + ); + } + + [Fact] + public void TestSeekBeyondCapacityWithResize() + { + var writer = new SpanWriter(10, true); + + var pos = writer.Seek(20, SeekOrigin.Begin); + + Assert.Equal(20, pos); + Assert.True(writer.Capacity >= 20); + writer.Dispose(); + } + + [Fact] + public void TestEnsureCapacityNoResize() + { + Span buffer = stackalloc byte[10]; + var writer = new SpanWriter(buffer); + + writer.EnsureCapacity(10); + Assert.Equal(10, writer.Capacity); + } + + [Fact] + public void TestEnsureCapacityNoResizeThrows() + { + Assert.Throws( + () => + { + Span buffer = stackalloc byte[10]; + var writer = new SpanWriter(buffer); + writer.EnsureCapacity(20); + } + ); + } + + [Fact] + public void TestEnsureCapacityWithResize() + { + var writer = new SpanWriter(10, true); + + writer.EnsureCapacity(50); + + Assert.True(writer.Capacity >= 50); + writer.Dispose(); + } + + [Fact] + public void TestToSpanWithStackAlloc() + { + Span buffer = stackalloc byte[10]; + var writer = new SpanWriter(buffer); + + writer.Write((byte)0x01); + writer.Write((byte)0x02); + writer.Write((byte)0x03); + + using var owner = writer.ToSpan(); + + Assert.Equal(3, owner.Span.Length); + AssertThat.Equal(owner.Span, [0x01, 0x02, 0x03]); + } + + [Fact] + public void TestToSpanWithRentedBuffer() + { + var writer = new SpanWriter(10); + + writer.Write((byte)0x01); + writer.Write((byte)0x02); + writer.Write((byte)0x03); + + using var owner = writer.ToSpan(); + + Assert.Equal(3, owner.Span.Length); + AssertThat.Equal(owner.Span, [0x01, 0x02, 0x03]); + } + + [Fact] + public void TestToSpanEmpty() + { + var writer = new SpanWriter(10); + + using var owner = writer.ToSpan(); + + Assert.Equal(0, owner.Span.Length); + } + + [Fact] + public void TestBytesWritten() + { + Span buffer = stackalloc byte[10]; + var writer = new SpanWriter(buffer); + + Assert.Equal(0, writer.BytesWritten); + + writer.Write((byte)0x01); + Assert.Equal(1, writer.BytesWritten); + + writer.Write((ushort)0x0203); + Assert.Equal(3, writer.BytesWritten); + + writer.Seek(1, SeekOrigin.Begin); + Assert.Equal(3, writer.BytesWritten); + + writer.Write((byte)0xFF); + Assert.Equal(3, writer.BytesWritten); + } + + [Fact] + public void TestCapacity() + { + Span buffer = stackalloc byte[10]; + var writer = new SpanWriter(buffer); + + Assert.Equal(10, writer.Capacity); + } + + [Fact] + public void TestRawBuffer() + { + Span buffer = stackalloc byte[10]; + var writer = new SpanWriter(buffer); + + writer.Write((byte)0x01); + + var raw = writer.RawBuffer; + Assert.Equal(10, raw.Length); + Assert.Equal(0x01, raw[0]); + } + + [Fact] + public void TestSpan() + { + Span buffer = stackalloc byte[10]; + var writer = new SpanWriter(buffer); + + writer.Write((byte)0x01); + writer.Write((byte)0x02); + + var span = writer.Span; + Assert.Equal(2, span.Length); + AssertThat.Equal(span, [0x01, 0x02]); } } diff --git a/Projects/Server/Buffers/SpanReader.cs b/Projects/Server/Buffers/SpanReader.cs index 290ba5a94..dd35e8702 100644 --- a/Projects/Server/Buffers/SpanReader.cs +++ b/Projects/Server/Buffers/SpanReader.cs @@ -275,7 +275,7 @@ public ref struct SpanReader } [MethodImpl(MethodImplOptions.AggressiveInlining)] - public int Read(Span bytes) + public int Read(scoped Span bytes) { if (bytes.Length == 0) {