diff --git a/CLAUDE.md b/CLAUDE.md
new file mode 100644
index 00000000..3db81d74
--- /dev/null
+++ b/CLAUDE.md
@@ -0,0 +1 @@
+Read AGENTS.md in the repository root and follow the instructions it contains.
diff --git a/SampSharp.slnx b/SampSharp.slnx
index 4f6b461f..755d1a72 100644
--- a/SampSharp.slnx
+++ b/SampSharp.slnx
@@ -20,6 +20,8 @@
+
+
diff --git a/src/SampSharp.CodeFixes/Sash0001ExtensionAttributeCodeFixProvider.cs b/src/SampSharp.CodeFixes/Sash0001ExtensionAttributeCodeFixProvider.cs
index 43d320fa..530088f2 100644
--- a/src/SampSharp.CodeFixes/Sash0001ExtensionAttributeCodeFixProvider.cs
+++ b/src/SampSharp.CodeFixes/Sash0001ExtensionAttributeCodeFixProvider.cs
@@ -84,8 +84,8 @@ private static async Task Fix(Document document, ClassDeclarationSynta
id)))))))));
var newRoot = root.ReplaceNode(classDeclaration, newClassDeclaration);
-
- if (root is CompilationUnitSyntax compilationUnit && compilationUnit.Usings.All(u => u.Name?.ToString() != "SampSharp.OpenMp.Core"))
+
+ if (newRoot is CompilationUnitSyntax compilationUnit && compilationUnit.Usings.All(u => u.Name?.ToString() != "SampSharp.OpenMp.Core"))
{
newRoot = compilationUnit.AddUsings(UsingDirective(ParseName("SampSharp.OpenMp.Core")));
}
diff --git a/src/SampSharp.CodeFixes/Sash0003MakeStructPartialCodeFixProvider.cs b/src/SampSharp.CodeFixes/Sash0003MakeStructPartialCodeFixProvider.cs
index a54329ea..6c09a6c7 100644
--- a/src/SampSharp.CodeFixes/Sash0003MakeStructPartialCodeFixProvider.cs
+++ b/src/SampSharp.CodeFixes/Sash0003MakeStructPartialCodeFixProvider.cs
@@ -65,7 +65,7 @@ private static async Task Fix(Document document, StructDeclarationSynt
{
newStructDeclaration = newStructDeclaration
.WithModifiers(
- structDeclaration.Modifiers.Add(
+ newStructDeclaration.Modifiers.Add(
Token(SyntaxKind.PartialKeyword)));
}
@@ -73,7 +73,7 @@ private static async Task Fix(Document document, StructDeclarationSynt
{
newStructDeclaration = newStructDeclaration
.WithModifiers(
- structDeclaration.Modifiers.Insert(0,
+ newStructDeclaration.Modifiers.Insert(0,
Token(SyntaxKind.ReadOnlyKeyword)));
}
diff --git a/src/SampSharp.OpenMp.Core/Api/Components/Classes/IPlayerClassData.cs b/src/SampSharp.OpenMp.Core/Api/Components/Classes/IPlayerClassData.cs
index dcdfa7c2..12ccd531 100644
--- a/src/SampSharp.OpenMp.Core/Api/Components/Classes/IPlayerClassData.cs
+++ b/src/SampSharp.OpenMp.Core/Api/Components/Classes/IPlayerClassData.cs
@@ -13,7 +13,7 @@ public readonly partial struct IPlayerClassData
/// Gets the player's current class information.
///
/// A reference to the player's class data.
- public partial ref PlayerClass GetClass();
+ public partial BlittableStructRef GetClass();
///
/// Sets the spawn information for the player's class.
diff --git a/src/SampSharp.OpenMp.Core/Api/Components/Vehicles/IVehicle.cs b/src/SampSharp.OpenMp.Core/Api/Components/Vehicles/IVehicle.cs
index 55ee19e1..3cd07dfd 100644
--- a/src/SampSharp.OpenMp.Core/Api/Components/Vehicles/IVehicle.cs
+++ b/src/SampSharp.OpenMp.Core/Api/Components/Vehicles/IVehicle.cs
@@ -259,8 +259,13 @@ public VehicleParams GetParams()
/// Gets the respawn delay of the vehicle.
///
/// The respawn delay of the vehicle.
- [return: MarshalUsing(typeof(SecondsMarshaller))]
- public partial TimeSpan GetRespawnDelay();
+ public TimeSpan GetRespawnDelay()
+ {
+ GetRespawnDelay(out var result);
+ return SecondsMarshaller.NativeToManaged.ConvertToManaged(result);
+ }
+
+ private partial void GetRespawnDelay(out Seconds result);
///
/// Sets the respawn delay of the vehicle.
diff --git a/src/SampSharp.OpenMp.Core/Api/Player/IPlayerPool.cs b/src/SampSharp.OpenMp.Core/Api/Player/IPlayerPool.cs
index 5d60a0c5..caf844ff 100644
--- a/src/SampSharp.OpenMp.Core/Api/Player/IPlayerPool.cs
+++ b/src/SampSharp.OpenMp.Core/Api/Player/IPlayerPool.cs
@@ -1,4 +1,5 @@
using System.Numerics;
+using System.Runtime.InteropServices;
using System.Runtime.InteropServices.Marshalling;
using SampSharp.OpenMp.Core.RobinHood;
using SampSharp.OpenMp.Core.Std;
@@ -198,7 +199,13 @@ public readonly partial struct IPlayerPool
///
/// The player ID.
/// The default colour.
- public partial Colour GetDefaultColour(int pid);
+ public Colour GetDefaultColour(int pid)
+ {
+ GetDefaultColour(pid, out var result);
+ return result;
+ }
+
+ private partial void GetDefaultColour(int pid, out Colour result);
///
/// Converts this instance to a read-only player pool.
diff --git a/src/SampSharp.OpenMp.Core/Std/Pair.cs b/src/SampSharp.OpenMp.Core/Std/Pair.cs
index 3137766c..34f08c54 100644
--- a/src/SampSharp.OpenMp.Core/Std/Pair.cs
+++ b/src/SampSharp.OpenMp.Core/Std/Pair.cs
@@ -22,6 +22,17 @@ public readonly struct Pair
///
public readonly T2 Second;
+ ///
+ /// Initializes a new instance of the struct.
+ ///
+ /// The first value in the pair.
+ /// The second value in the pair.
+ public Pair(T1 first, T2 second)
+ {
+ First = first;
+ Second = second;
+ }
+
///
/// Deconstructs the pair into its two values.
///
@@ -54,6 +65,6 @@ public static implicit operator (T1, T2)(Pair pair)
/// The tuple to convert.
public static implicit operator Pair((T1 first,T2 second) tuple)
{
- return (tuple.first, tuple.second);
+ return new Pair(tuple.first, tuple.second);
}
}
diff --git a/src/SampSharp.OpenMp.Entities/SAMP/Components/Player.cs b/src/SampSharp.OpenMp.Entities/SAMP/Components/Player.cs
index fa746814..7dd45b9e 100644
--- a/src/SampSharp.OpenMp.Entities/SAMP/Components/Player.cs
+++ b/src/SampSharp.OpenMp.Entities/SAMP/Components/Player.cs
@@ -812,7 +812,7 @@ public virtual void SetSpawnInfo(PlayerSpawnData spawnData)
/// A instance containing the player's spawn position, orientation, and related data.
public virtual PlayerSpawnData GetSpawnInfo()
{
- ref var data = ref ClassData.GetClass();
+ var data = ClassData.GetClass().Value;
return PlayerSpawnData.FromOmpData(ref data);
}
diff --git a/src/SampSharp.OpenMp.Entities/SAMP/Components/PlayerPickup.cs b/src/SampSharp.OpenMp.Entities/SAMP/Components/PlayerPickup.cs
index b6dfa072..2c2b8b97 100644
--- a/src/SampSharp.OpenMp.Entities/SAMP/Components/PlayerPickup.cs
+++ b/src/SampSharp.OpenMp.Entities/SAMP/Components/PlayerPickup.cs
@@ -43,13 +43,4 @@ public virtual void StreamOut()
{
_pickup.StreamOutForPlayer(_player);
}
-
- ///
- /// Gets or sets a value indicating whether the pickup is hidden for the player.
- ///
- public virtual bool IsHidden
- {
- get => _pickup.IsPickupHiddenForPlayer(_player);
- set => _pickup.SetPickupHiddenForPlayer(_player, value);
- }
}
diff --git a/src/SampSharp.OpenMp.Entities/SAMP/Components/Vehicle.cs b/src/SampSharp.OpenMp.Entities/SAMP/Components/Vehicle.cs
index 3ad1e74d..5ca89327 100644
--- a/src/SampSharp.OpenMp.Entities/SAMP/Components/Vehicle.cs
+++ b/src/SampSharp.OpenMp.Entities/SAMP/Components/Vehicle.cs
@@ -761,6 +761,12 @@ protected override void OnDestroyComponent()
///
public override string ToString()
{
+ if (IsDestroying)
+ {
+ // TODO: do this check for other components as well
+ return "(Destroyed)";
+ }
+
return $"(Id: {Id}, Model: {Model})";
}
diff --git a/src/sampsharp-component/proxies/api.cpp b/src/sampsharp-component/proxies/api.cpp
index 3ffc1a9c..1c8e3de9 100644
--- a/src/sampsharp-component/proxies/api.cpp
+++ b/src/sampsharp-component/proxies/api.cpp
@@ -19,6 +19,7 @@
#include
#include "../proxy-api.hpp"
+#include
#ifdef __clang__
#pragma clang diagnostic push
@@ -376,18 +377,18 @@ PROXY(INPCComponent, void, destroy, INPC&);
PROXY(INPCComponent, int, createPath);
PROXY(INPCComponent, bool, destroyPath, int);
PROXY(INPCComponent, void, destroyAllPaths);
-PROXY_PTR(INPCComponent, size_t, getPathCount);
+PROXY(INPCComponent, size_t, getPathCount);
PROXY(INPCComponent, bool, addPointToPath, int, const Vector3&, float);
PROXY(INPCComponent, bool, removePointFromPath, int, size_t);
PROXY(INPCComponent, bool, clearPath, int);
-PROXY_PTR(INPCComponent, size_t, getPathPointCount, int);
+PROXY(INPCComponent, size_t, getPathPointCount, int);
PROXY(INPCComponent, bool, getPathPoint, int, size_t, Vector3&, float&);
PROXY(INPCComponent, bool, hasPathPointInRange, int, const Vector3&, float);
PROXY(INPCComponent, bool, isValidPath, int);
PROXY(INPCComponent, int, loadRecord, StringView);
PROXY(INPCComponent, bool, unloadRecord, int);
PROXY(INPCComponent, bool, isValidRecord, int);
-PROXY_PTR(INPCComponent, size_t, getRecordCount);
+PROXY(INPCComponent, size_t, getRecordCount);
PROXY(INPCComponent, void, unloadAllRecords);
PROXY(INPCComponent, bool, openNode, int);
PROXY(INPCComponent, void, closeNode, int);
@@ -675,7 +676,7 @@ PROXY(IVehicle, void, setParamsForPlayer, IPlayer&, VehicleParams&);
PROXY_PTR(IVehicle, VehicleParams, getParams);
PROXY(IVehicle, bool, isDead);
PROXY(IVehicle, void, respawn);
-PROXY(IVehicle, Seconds, getRespawnDelay);
+PROXY_PTR(IVehicle, Seconds, getRespawnDelay);
PROXY(IVehicle, void, setRespawnDelay, Seconds);
PROXY(IVehicle, bool, isRespawning);
PROXY(IVehicle, void, setInterior, int);
@@ -1032,7 +1033,7 @@ PROXY(IPlayerPool, void, broadcastRPC, int, Span, int, const IPlayer*,
PROXY(IPlayerPool, bool, isNameValid, StringView);
PROXY(IPlayerPool, void, allowNickNameCharacter, char, bool);
PROXY(IPlayerPool, bool, isNickNameCharacterAllowed, char);
-PROXY(IPlayerPool, Colour, getDefaultColour, int);
+PROXY_PTR(IPlayerPool, Colour, getDefaultColour, int);
PROXY_CAST_NAMED(IPlayerPool, IPlayerPool, IReadOnlyPool, IReadOnlyPool);
PROXY_EVENT_DISPATCHER(IPlayerPool, PlayerSpawnEventHandler, getPlayerSpawnDispatcher);
diff --git a/test/SampSharp.Analyzer.Tests/AnalyzerTestHelper.cs b/test/SampSharp.Analyzer.Tests/AnalyzerTestHelper.cs
new file mode 100644
index 00000000..5c9e553d
--- /dev/null
+++ b/test/SampSharp.Analyzer.Tests/AnalyzerTestHelper.cs
@@ -0,0 +1,74 @@
+using System.Collections.Immutable;
+using System.Reflection;
+using System.Runtime.InteropServices;
+using System.Runtime.InteropServices.Marshalling;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp;
+using Microsoft.CodeAnalysis.Diagnostics;
+using SampSharp.OpenMp.Core;
+
+namespace SampSharp.Analyzer.Tests;
+
+///
+/// Lightweight helper that compiles a C# source string with the SampSharp.OpenMp.Core
+/// references available and runs an analyzer against it.
+///
+internal static class AnalyzerTestHelper
+{
+ private static readonly Lazy> _references = new(BuildReferences);
+
+ public static async Task> GetDiagnosticsAsync(
+ DiagnosticAnalyzer analyzer,
+ string source,
+ bool allowUnsafe = true)
+ {
+ var compilation = CreateCompilation(source, allowUnsafe);
+ var withAnalyzers = compilation.WithAnalyzers(ImmutableArray.Create(analyzer));
+ var diagnostics = await withAnalyzers.GetAnalyzerDiagnosticsAsync().ConfigureAwait(false);
+ return diagnostics;
+ }
+
+ public static CSharpCompilation CreateCompilation(string source, bool allowUnsafe = true, string assemblyName = "TestCompilation")
+ {
+ var syntaxTree = CSharpSyntaxTree.ParseText(source);
+
+ return CSharpCompilation.Create(
+ assemblyName,
+ syntaxTrees: [syntaxTree],
+ references: _references.Value,
+ options: new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary, allowUnsafe: allowUnsafe));
+ }
+
+ private static IReadOnlyList BuildReferences()
+ {
+ var runtimeDir = Path.GetDirectoryName(typeof(object).Assembly.Location)!;
+ var refs = new List();
+
+ void Add(Assembly assembly)
+ {
+ if (!string.IsNullOrEmpty(assembly.Location) && File.Exists(assembly.Location))
+ {
+ refs.Add(MetadataReference.CreateFromFile(assembly.Location));
+ }
+ }
+
+ Add(typeof(object).Assembly);
+ Add(typeof(Attribute).Assembly);
+ Add(typeof(Console).Assembly);
+ Add(typeof(Marshal).Assembly);
+ Add(typeof(CustomMarshallerAttribute).Assembly);
+
+ foreach (var name in new[] { "System.Runtime.dll", "System.Collections.dll", "netstandard.dll" })
+ {
+ var path = Path.Combine(runtimeDir, name);
+ if (File.Exists(path))
+ {
+ refs.Add(MetadataReference.CreateFromFile(path));
+ }
+ }
+
+ refs.Add(MetadataReference.CreateFromFile(typeof(OpenMpApiAttribute).Assembly.Location));
+
+ return refs;
+ }
+}
diff --git a/test/SampSharp.Analyzer.Tests/SampSharp.Analyzer.Tests.csproj b/test/SampSharp.Analyzer.Tests/SampSharp.Analyzer.Tests.csproj
new file mode 100644
index 00000000..3599d14d
--- /dev/null
+++ b/test/SampSharp.Analyzer.Tests/SampSharp.Analyzer.Tests.csproj
@@ -0,0 +1,23 @@
+
+
+
+ Exe
+
+
+
+
+
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+
+
+
+
+
+
+
diff --git a/test/SampSharp.Analyzer.Tests/Sash0001ExtensionAttributeAnalyzerTests.cs b/test/SampSharp.Analyzer.Tests/Sash0001ExtensionAttributeAnalyzerTests.cs
new file mode 100644
index 00000000..200de50e
--- /dev/null
+++ b/test/SampSharp.Analyzer.Tests/Sash0001ExtensionAttributeAnalyzerTests.cs
@@ -0,0 +1,72 @@
+using SampSharp.Analyzer;
+using SampSharp.Analyzer.Analyzers;
+using Shouldly;
+using Xunit;
+
+namespace SampSharp.Analyzer.Tests;
+
+public class Sash0001ExtensionAttributeAnalyzerTests
+{
+ [Fact]
+ public async Task Sash0001_should_report_when_class_extends_Extension_without_ExtensionAttribute()
+ {
+ const string source = """
+ using SampSharp.OpenMp.Core;
+
+ public class MyExt : Extension { }
+ """;
+
+ var diags = await AnalyzerTestHelper.GetDiagnosticsAsync(new Sash0001ExtensionAttributeAnalyzer(), source);
+
+ var match = diags.Where(d => d.Id == AnalyzerIds.Sash0001MissingExtensionAttribute.Id).ToList();
+ match.Count.ShouldBe(1);
+ match[0].GetMessage().ShouldContain("MyExt");
+ }
+
+ [Fact]
+ public async Task Sash0001_should_not_report_when_ExtensionAttribute_is_present()
+ {
+ const string source = """
+ using SampSharp.OpenMp.Core;
+
+ [Extension(0x1234)]
+ public class MyExt : Extension { }
+ """;
+
+ var diags = await AnalyzerTestHelper.GetDiagnosticsAsync(new Sash0001ExtensionAttributeAnalyzer(), source);
+
+ diags.ShouldNotContain(d => d.Id == AnalyzerIds.Sash0001MissingExtensionAttribute.Id);
+ }
+
+ [Fact]
+ public async Task Sash0001_should_not_report_when_class_does_not_extend_Extension()
+ {
+ const string source = """
+ public class Plain { }
+ """;
+
+ var diags = await AnalyzerTestHelper.GetDiagnosticsAsync(new Sash0001ExtensionAttributeAnalyzer(), source);
+
+ diags.ShouldNotContain(d => d.Id == AnalyzerIds.Sash0001MissingExtensionAttribute.Id);
+ }
+
+ [Fact]
+ public async Task Sash0001_should_not_report_for_unrelated_ExtensionAttribute_type()
+ {
+ // Compilation references Core, so this path is exercised when the symbol isn't found.
+ // We simulate by referencing a class that just happens to be named ExtensionAttribute
+ // in a different namespace — the analyzer should look up by fully qualified name and
+ // only match the SampSharp one.
+ const string source = """
+ namespace Other;
+
+ public class ExtensionAttribute : System.Attribute { }
+
+ public class NotAnExtension { }
+ """;
+
+ var diags = await AnalyzerTestHelper.GetDiagnosticsAsync(new Sash0001ExtensionAttributeAnalyzer(), source);
+
+ diags.ShouldNotContain(d => d.Id == AnalyzerIds.Sash0001MissingExtensionAttribute.Id);
+ }
+}
diff --git a/test/SampSharp.Analyzer.Tests/Sash0002EventHandlerWithGenericParametersAnalyzerTests.cs b/test/SampSharp.Analyzer.Tests/Sash0002EventHandlerWithGenericParametersAnalyzerTests.cs
new file mode 100644
index 00000000..d6ed686f
--- /dev/null
+++ b/test/SampSharp.Analyzer.Tests/Sash0002EventHandlerWithGenericParametersAnalyzerTests.cs
@@ -0,0 +1,56 @@
+using SampSharp.Analyzer;
+using SampSharp.Analyzer.Analyzers;
+using Shouldly;
+using Xunit;
+
+namespace SampSharp.Analyzer.Tests;
+
+public class Sash0002EventHandlerWithGenericParametersAnalyzerTests
+{
+ [Fact]
+ public async Task Sash0002_should_report_when_event_handler_interface_has_type_parameters()
+ {
+ const string source = """
+ using SampSharp.OpenMp.Core;
+
+ [OpenMpEventHandler]
+ public interface IMyHandler { }
+ """;
+
+ var diags = await AnalyzerTestHelper.GetDiagnosticsAsync(
+ new Sash0002EventHandlerWithGenericParametersAnalyzer(), source);
+
+ var match = diags.Where(d => d.Id == AnalyzerIds.Sash0002GenericEventHandlerUnsupported.Id).ToList();
+ match.Count.ShouldBe(1);
+ match[0].GetMessage().ShouldContain("IMyHandler");
+ }
+
+ [Fact]
+ public async Task Sash0002_should_not_report_for_non_generic_event_handler()
+ {
+ const string source = """
+ using SampSharp.OpenMp.Core;
+
+ [OpenMpEventHandler]
+ public interface IMyHandler { }
+ """;
+
+ var diags = await AnalyzerTestHelper.GetDiagnosticsAsync(
+ new Sash0002EventHandlerWithGenericParametersAnalyzer(), source);
+
+ diags.ShouldNotContain(d => d.Id == AnalyzerIds.Sash0002GenericEventHandlerUnsupported.Id);
+ }
+
+ [Fact]
+ public async Task Sash0002_should_not_report_for_generic_interface_without_attribute()
+ {
+ const string source = """
+ public interface IPlainGeneric { }
+ """;
+
+ var diags = await AnalyzerTestHelper.GetDiagnosticsAsync(
+ new Sash0002EventHandlerWithGenericParametersAnalyzer(), source);
+
+ diags.ShouldNotContain(d => d.Id == AnalyzerIds.Sash0002GenericEventHandlerUnsupported.Id);
+ }
+}
diff --git a/test/SampSharp.Analyzer.Tests/Sash0003ApiStructMustBeReadonlyPartialAnalyzerTests.cs b/test/SampSharp.Analyzer.Tests/Sash0003ApiStructMustBeReadonlyPartialAnalyzerTests.cs
new file mode 100644
index 00000000..1b45233c
--- /dev/null
+++ b/test/SampSharp.Analyzer.Tests/Sash0003ApiStructMustBeReadonlyPartialAnalyzerTests.cs
@@ -0,0 +1,86 @@
+using SampSharp.Analyzer;
+using SampSharp.Analyzer.Analyzers;
+using Shouldly;
+using Xunit;
+
+namespace SampSharp.Analyzer.Tests;
+
+public class Sash0003ApiStructMustBeReadonlyPartialAnalyzerTests
+{
+ [Fact]
+ public async Task Sash0003_should_report_when_api_struct_missing_partial()
+ {
+ const string source = """
+ using SampSharp.OpenMp.Core;
+
+ [OpenMpApi]
+ public readonly struct MyApi { }
+ """;
+
+ var diags = await AnalyzerTestHelper.GetDiagnosticsAsync(
+ new Sash0003ApiStructMustBeReadonlyPartialAnalyzer(), source);
+
+ diags.Count(d => d.Id == AnalyzerIds.Sash0003ApiStructMustBeReadonlyPartial.Id).ShouldBe(1);
+ }
+
+ [Fact]
+ public async Task Sash0003_should_report_when_api_struct_missing_readonly()
+ {
+ const string source = """
+ using SampSharp.OpenMp.Core;
+
+ [OpenMpApi]
+ public partial struct MyApi { }
+ """;
+
+ var diags = await AnalyzerTestHelper.GetDiagnosticsAsync(
+ new Sash0003ApiStructMustBeReadonlyPartialAnalyzer(), source);
+
+ diags.Count(d => d.Id == AnalyzerIds.Sash0003ApiStructMustBeReadonlyPartial.Id).ShouldBe(1);
+ }
+
+ [Fact]
+ public async Task Sash0003_should_report_when_api_struct_missing_both()
+ {
+ const string source = """
+ using SampSharp.OpenMp.Core;
+
+ [OpenMpApi]
+ public struct MyApi { }
+ """;
+
+ var diags = await AnalyzerTestHelper.GetDiagnosticsAsync(
+ new Sash0003ApiStructMustBeReadonlyPartialAnalyzer(), source);
+
+ diags.Count(d => d.Id == AnalyzerIds.Sash0003ApiStructMustBeReadonlyPartial.Id).ShouldBe(1);
+ }
+
+ [Fact]
+ public async Task Sash0003_should_not_report_when_api_struct_is_readonly_partial()
+ {
+ const string source = """
+ using SampSharp.OpenMp.Core;
+
+ [OpenMpApi]
+ public readonly partial struct MyApi { }
+ """;
+
+ var diags = await AnalyzerTestHelper.GetDiagnosticsAsync(
+ new Sash0003ApiStructMustBeReadonlyPartialAnalyzer(), source);
+
+ diags.ShouldNotContain(d => d.Id == AnalyzerIds.Sash0003ApiStructMustBeReadonlyPartial.Id);
+ }
+
+ [Fact]
+ public async Task Sash0003_should_not_report_for_non_api_struct()
+ {
+ const string source = """
+ public struct Plain { }
+ """;
+
+ var diags = await AnalyzerTestHelper.GetDiagnosticsAsync(
+ new Sash0003ApiStructMustBeReadonlyPartialAnalyzer(), source);
+
+ diags.ShouldNotContain(d => d.Id == AnalyzerIds.Sash0003ApiStructMustBeReadonlyPartial.Id);
+ }
+}
diff --git a/test/SampSharp.Analyzer.Tests/Sash0004ApiStructMethodMarshalRefReturnTests.cs b/test/SampSharp.Analyzer.Tests/Sash0004ApiStructMethodMarshalRefReturnTests.cs
new file mode 100644
index 00000000..8f5da87f
--- /dev/null
+++ b/test/SampSharp.Analyzer.Tests/Sash0004ApiStructMethodMarshalRefReturnTests.cs
@@ -0,0 +1,99 @@
+using SampSharp.Analyzer;
+using SampSharp.Analyzer.Analyzers;
+using Shouldly;
+using Xunit;
+
+namespace SampSharp.Analyzer.Tests;
+
+public class Sash0004ApiStructMethodMarshalRefReturnTests
+{
+ [Fact]
+ public async Task Sash0004_should_report_when_partial_method_uses_ref_return_with_MarshalUsing()
+ {
+ const string source = """
+ using System.Runtime.InteropServices.Marshalling;
+ using SampSharp.OpenMp.Core;
+
+ public class Dummy : CustomMarshaller { }
+
+ public static class FakeMarshaller { }
+
+ [OpenMpApi]
+ public readonly partial struct MyApi
+ {
+ [return: MarshalUsing(typeof(FakeMarshaller))]
+ public partial ref int GetValue();
+ }
+ """;
+
+ var diags = await AnalyzerTestHelper.GetDiagnosticsAsync(
+ new Sash0004ApiStructMethodMarshalRefReturnNotSupportedAnalyzer(), source);
+
+ diags.Count(d => d.Id == AnalyzerIds.Sash0004ApiStructMarshalRefReturnUnsupported.Id).ShouldBe(1);
+ }
+
+ [Fact]
+ public async Task Sash0004_should_report_when_partial_method_uses_ref_return_with_NativeMarshalling_target()
+ {
+ const string source = """
+ using System.Runtime.InteropServices.Marshalling;
+ using SampSharp.OpenMp.Core;
+
+ public static class FakeMarshaller { }
+
+ [NativeMarshalling(typeof(FakeMarshaller))]
+ public struct SomeStruct { }
+
+ [OpenMpApi]
+ public readonly partial struct MyApi
+ {
+ public partial ref SomeStruct GetValue();
+ }
+ """;
+
+ var diags = await AnalyzerTestHelper.GetDiagnosticsAsync(
+ new Sash0004ApiStructMethodMarshalRefReturnNotSupportedAnalyzer(), source);
+
+ diags.Count(d => d.Id == AnalyzerIds.Sash0004ApiStructMarshalRefReturnUnsupported.Id).ShouldBe(1);
+ }
+
+ [Fact]
+ public async Task Sash0004_should_not_report_for_ref_return_without_marshalling()
+ {
+ const string source = """
+ using SampSharp.OpenMp.Core;
+
+ [OpenMpApi]
+ public readonly partial struct MyApi
+ {
+ public partial ref int GetValue();
+ }
+ """;
+
+ var diags = await AnalyzerTestHelper.GetDiagnosticsAsync(
+ new Sash0004ApiStructMethodMarshalRefReturnNotSupportedAnalyzer(), source);
+
+ diags.ShouldNotContain(d => d.Id == AnalyzerIds.Sash0004ApiStructMarshalRefReturnUnsupported.Id);
+ }
+
+ [Fact]
+ public async Task Sash0004_should_not_report_for_non_api_struct()
+ {
+ const string source = """
+ using System.Runtime.InteropServices.Marshalling;
+
+ public static class FakeMarshaller { }
+
+ public readonly partial struct Plain
+ {
+ [return: MarshalUsing(typeof(FakeMarshaller))]
+ public partial ref int GetValue();
+ }
+ """;
+
+ var diags = await AnalyzerTestHelper.GetDiagnosticsAsync(
+ new Sash0004ApiStructMethodMarshalRefReturnNotSupportedAnalyzer(), source);
+
+ diags.ShouldNotContain(d => d.Id == AnalyzerIds.Sash0004ApiStructMarshalRefReturnUnsupported.Id);
+ }
+}
diff --git a/test/SampSharp.Analyzer.Tests/Sash0005ApiStructRequiresAllowUnsafeBlocksTests.cs b/test/SampSharp.Analyzer.Tests/Sash0005ApiStructRequiresAllowUnsafeBlocksTests.cs
new file mode 100644
index 00000000..ed5e2842
--- /dev/null
+++ b/test/SampSharp.Analyzer.Tests/Sash0005ApiStructRequiresAllowUnsafeBlocksTests.cs
@@ -0,0 +1,57 @@
+using SampSharp.Analyzer;
+using SampSharp.Analyzer.Analyzers;
+using Shouldly;
+using Xunit;
+
+namespace SampSharp.Analyzer.Tests;
+
+public class Sash0005ApiStructRequiresAllowUnsafeBlocksTests
+{
+ [Fact]
+ public async Task Sash0005_should_report_when_AllowUnsafe_is_false()
+ {
+ const string source = """
+ using SampSharp.OpenMp.Core;
+
+ [OpenMpApi]
+ public readonly partial struct MyApi { }
+ """;
+
+ var diags = await AnalyzerTestHelper.GetDiagnosticsAsync(
+ new Sash0005ApiStructRequiresAllowUnsafeBlocks(), source, allowUnsafe: false);
+
+ diags.Count(d => d.Id == AnalyzerIds.Sash0005ApiStructRequiresAllowUnsafeBlocks.Id).ShouldBe(1);
+ }
+
+ [Fact]
+ public async Task Sash0005_should_not_report_when_AllowUnsafe_is_true()
+ {
+ const string source = """
+ using SampSharp.OpenMp.Core;
+
+ [OpenMpApi]
+ public readonly partial struct MyApi { }
+ """;
+
+ var diags = await AnalyzerTestHelper.GetDiagnosticsAsync(
+ new Sash0005ApiStructRequiresAllowUnsafeBlocks(), source, allowUnsafe: true);
+
+ diags.ShouldNotContain(d => d.Id == AnalyzerIds.Sash0005ApiStructRequiresAllowUnsafeBlocks.Id);
+ }
+
+ [Fact]
+ public async Task Sash0005_should_not_report_for_attributes_other_than_OpenMpApi()
+ {
+ const string source = """
+ using System;
+
+ [Serializable]
+ public class Plain { }
+ """;
+
+ var diags = await AnalyzerTestHelper.GetDiagnosticsAsync(
+ new Sash0005ApiStructRequiresAllowUnsafeBlocks(), source, allowUnsafe: false);
+
+ diags.ShouldNotContain(d => d.Id == AnalyzerIds.Sash0005ApiStructRequiresAllowUnsafeBlocks.Id);
+ }
+}
diff --git a/test/SampSharp.Analyzer.Tests/Sash0006ApiStructBaseTypeMustBeApiStructTests.cs b/test/SampSharp.Analyzer.Tests/Sash0006ApiStructBaseTypeMustBeApiStructTests.cs
new file mode 100644
index 00000000..139325e1
--- /dev/null
+++ b/test/SampSharp.Analyzer.Tests/Sash0006ApiStructBaseTypeMustBeApiStructTests.cs
@@ -0,0 +1,86 @@
+using SampSharp.Analyzer;
+using SampSharp.Analyzer.Analyzers;
+using Shouldly;
+using Xunit;
+
+namespace SampSharp.Analyzer.Tests;
+
+public class Sash0006ApiStructBaseTypeMustBeApiStructTests
+{
+ [Fact]
+ public async Task Sash0006_should_report_when_base_type_in_OpenMpApi_argument_is_not_an_api_struct()
+ {
+ const string source = """
+ using SampSharp.OpenMp.Core;
+
+ public readonly partial struct NotApi { }
+
+ [OpenMpApi(typeof(NotApi))]
+ public readonly partial struct MyApi { }
+ """;
+
+ var diags = await AnalyzerTestHelper.GetDiagnosticsAsync(
+ new Sash0006ApiStructBaseTypeMustBeApiStruct(), source);
+
+ var match = diags.Where(d => d.Id == AnalyzerIds.Sash0006ApiStructBaseTypeMustBeApiStruct.Id).ToList();
+ match.Count.ShouldBe(1);
+ match[0].GetMessage().ShouldContain("NotApi");
+ match[0].GetMessage().ShouldContain("MyApi");
+ }
+
+ [Fact]
+ public async Task Sash0006_should_not_report_when_base_type_is_an_api_struct()
+ {
+ const string source = """
+ using SampSharp.OpenMp.Core;
+
+ [OpenMpApi]
+ public readonly partial struct BaseApi { }
+
+ [OpenMpApi(typeof(BaseApi))]
+ public readonly partial struct MyApi { }
+ """;
+
+ var diags = await AnalyzerTestHelper.GetDiagnosticsAsync(
+ new Sash0006ApiStructBaseTypeMustBeApiStruct(), source);
+
+ diags.ShouldNotContain(d => d.Id == AnalyzerIds.Sash0006ApiStructBaseTypeMustBeApiStruct.Id);
+ }
+
+ [Fact]
+ public async Task Sash0006_should_not_report_when_OpenMpApi_has_no_arguments()
+ {
+ const string source = """
+ using SampSharp.OpenMp.Core;
+
+ [OpenMpApi]
+ public readonly partial struct MyApi { }
+ """;
+
+ var diags = await AnalyzerTestHelper.GetDiagnosticsAsync(
+ new Sash0006ApiStructBaseTypeMustBeApiStruct(), source);
+
+ diags.ShouldNotContain(d => d.Id == AnalyzerIds.Sash0006ApiStructBaseTypeMustBeApiStruct.Id);
+ }
+
+ [Fact]
+ public async Task Sash0006_should_not_report_for_unrelated_attribute()
+ {
+ const string source = """
+ using System;
+
+ [AttributeUsage(AttributeTargets.All)]
+ public class Other : Attribute { }
+
+ public readonly partial struct NotApi { }
+
+ [Other]
+ public readonly partial struct MyApi { }
+ """;
+
+ var diags = await AnalyzerTestHelper.GetDiagnosticsAsync(
+ new Sash0006ApiStructBaseTypeMustBeApiStruct(), source);
+
+ diags.ShouldNotContain(d => d.Id == AnalyzerIds.Sash0006ApiStructBaseTypeMustBeApiStruct.Id);
+ }
+}
diff --git a/test/SampSharp.Analyzer.Tests/Sash0007ApiStructMustNotContainFieldsTests.cs b/test/SampSharp.Analyzer.Tests/Sash0007ApiStructMustNotContainFieldsTests.cs
new file mode 100644
index 00000000..a756821c
--- /dev/null
+++ b/test/SampSharp.Analyzer.Tests/Sash0007ApiStructMustNotContainFieldsTests.cs
@@ -0,0 +1,101 @@
+using SampSharp.Analyzer;
+using SampSharp.Analyzer.Analyzers;
+using Shouldly;
+using Xunit;
+
+namespace SampSharp.Analyzer.Tests;
+
+public class Sash0007ApiStructMustNotContainFieldsTests
+{
+ [Fact]
+ public async Task Sash0007_should_report_when_api_struct_contains_field()
+ {
+ const string source = """
+ using SampSharp.OpenMp.Core;
+
+ [OpenMpApi]
+ public readonly partial struct MyApi
+ {
+ public readonly int Value;
+ }
+ """;
+
+ var diags = await AnalyzerTestHelper.GetDiagnosticsAsync(
+ new Sash0007ApiStructMustNotContainFields(), source);
+
+ diags.Count(d => d.Id == AnalyzerIds.Sash0007ApiStructMustNotContainFields.Id).ShouldBe(1);
+ }
+
+ [Fact]
+ public async Task Sash0007_should_report_when_api_struct_contains_auto_property()
+ {
+ const string source = """
+ using SampSharp.OpenMp.Core;
+
+ [OpenMpApi]
+ public readonly partial struct MyApi
+ {
+ public int Value { get; }
+ }
+ """;
+
+ var diags = await AnalyzerTestHelper.GetDiagnosticsAsync(
+ new Sash0007ApiStructMustNotContainFields(), source);
+
+ diags.Count(d => d.Id == AnalyzerIds.Sash0007ApiStructMustNotContainFields.Id).ShouldBe(1);
+ }
+
+ [Fact]
+ public async Task Sash0007_should_not_report_for_expression_bodied_property()
+ {
+ const string source = """
+ using SampSharp.OpenMp.Core;
+
+ [OpenMpApi]
+ public readonly partial struct MyApi
+ {
+ public int Value => 0;
+ }
+ """;
+
+ var diags = await AnalyzerTestHelper.GetDiagnosticsAsync(
+ new Sash0007ApiStructMustNotContainFields(), source);
+
+ diags.ShouldNotContain(d => d.Id == AnalyzerIds.Sash0007ApiStructMustNotContainFields.Id);
+ }
+
+ [Fact]
+ public async Task Sash0007_should_not_report_for_methods_only()
+ {
+ const string source = """
+ using SampSharp.OpenMp.Core;
+
+ [OpenMpApi]
+ public readonly partial struct MyApi
+ {
+ public partial void DoThing();
+ }
+ """;
+
+ var diags = await AnalyzerTestHelper.GetDiagnosticsAsync(
+ new Sash0007ApiStructMustNotContainFields(), source);
+
+ diags.ShouldNotContain(d => d.Id == AnalyzerIds.Sash0007ApiStructMustNotContainFields.Id);
+ }
+
+ [Fact]
+ public async Task Sash0007_should_not_report_for_non_api_struct_with_field()
+ {
+ const string source = """
+ public readonly partial struct Plain
+ {
+ public readonly int Value;
+ }
+ """;
+
+ var diags = await AnalyzerTestHelper.GetDiagnosticsAsync(
+ new Sash0007ApiStructMustNotContainFields(), source);
+
+ diags.ShouldNotContain(d => d.Id == AnalyzerIds.Sash0007ApiStructMustNotContainFields.Id);
+ }
+}
diff --git a/test/SampSharp.Analyzer.Tests/Sash0008EventHandlerMarshalRefReturnTests.cs b/test/SampSharp.Analyzer.Tests/Sash0008EventHandlerMarshalRefReturnTests.cs
new file mode 100644
index 00000000..4dab4c8e
--- /dev/null
+++ b/test/SampSharp.Analyzer.Tests/Sash0008EventHandlerMarshalRefReturnTests.cs
@@ -0,0 +1,97 @@
+using SampSharp.Analyzer;
+using SampSharp.Analyzer.Analyzers;
+using Shouldly;
+using Xunit;
+
+namespace SampSharp.Analyzer.Tests;
+
+public class Sash0008EventHandlerMarshalRefReturnTests
+{
+ [Fact]
+ public async Task Sash0008_should_report_for_event_handler_method_with_ref_return_and_MarshalUsing()
+ {
+ const string source = """
+ using System.Runtime.InteropServices.Marshalling;
+ using SampSharp.OpenMp.Core;
+
+ public static class FakeMarshaller { }
+
+ [OpenMpEventHandler]
+ public interface IMyHandler
+ {
+ [return: MarshalUsing(typeof(FakeMarshaller))]
+ ref int GetValue();
+ }
+ """;
+
+ var diags = await AnalyzerTestHelper.GetDiagnosticsAsync(
+ new Sash0008EventHandlerMarshalRefReturnUnsupported(), source);
+
+ diags.Count(d => d.Id == AnalyzerIds.Sash0008EventHandlerMarshalRefReturnUnsupported.Id).ShouldBe(1);
+ }
+
+ [Fact]
+ public async Task Sash0008_should_report_for_event_handler_method_with_ref_return_and_NativeMarshalling_target()
+ {
+ const string source = """
+ using System.Runtime.InteropServices.Marshalling;
+ using SampSharp.OpenMp.Core;
+
+ public static class FakeMarshaller { }
+
+ [NativeMarshalling(typeof(FakeMarshaller))]
+ public struct SomeStruct { }
+
+ [OpenMpEventHandler]
+ public interface IMyHandler
+ {
+ ref SomeStruct GetValue();
+ }
+ """;
+
+ var diags = await AnalyzerTestHelper.GetDiagnosticsAsync(
+ new Sash0008EventHandlerMarshalRefReturnUnsupported(), source);
+
+ diags.Count(d => d.Id == AnalyzerIds.Sash0008EventHandlerMarshalRefReturnUnsupported.Id).ShouldBe(1);
+ }
+
+ [Fact]
+ public async Task Sash0008_should_not_report_for_ref_return_without_marshalling()
+ {
+ const string source = """
+ using SampSharp.OpenMp.Core;
+
+ [OpenMpEventHandler]
+ public interface IMyHandler
+ {
+ ref int GetValue();
+ }
+ """;
+
+ var diags = await AnalyzerTestHelper.GetDiagnosticsAsync(
+ new Sash0008EventHandlerMarshalRefReturnUnsupported(), source);
+
+ diags.ShouldNotContain(d => d.Id == AnalyzerIds.Sash0008EventHandlerMarshalRefReturnUnsupported.Id);
+ }
+
+ [Fact]
+ public async Task Sash0008_should_not_report_for_non_event_handler_interface()
+ {
+ const string source = """
+ using System.Runtime.InteropServices.Marshalling;
+
+ public static class FakeMarshaller { }
+
+ public interface IPlain
+ {
+ [return: MarshalUsing(typeof(FakeMarshaller))]
+ ref int GetValue();
+ }
+ """;
+
+ var diags = await AnalyzerTestHelper.GetDiagnosticsAsync(
+ new Sash0008EventHandlerMarshalRefReturnUnsupported(), source);
+
+ diags.ShouldNotContain(d => d.Id == AnalyzerIds.Sash0008EventHandlerMarshalRefReturnUnsupported.Id);
+ }
+}
diff --git a/test/SampSharp.Analyzer.Tests/SupportedDiagnosticsTests.cs b/test/SampSharp.Analyzer.Tests/SupportedDiagnosticsTests.cs
new file mode 100644
index 00000000..00e2d27e
--- /dev/null
+++ b/test/SampSharp.Analyzer.Tests/SupportedDiagnosticsTests.cs
@@ -0,0 +1,75 @@
+using Microsoft.CodeAnalysis.Diagnostics;
+using SampSharp.Analyzer;
+using SampSharp.Analyzer.Analyzers;
+using Shouldly;
+using Xunit;
+
+namespace SampSharp.Analyzer.Tests;
+
+public class SupportedDiagnosticsTests
+{
+ public static TheoryData Cases() => new()
+ {
+ { new Sash0001ExtensionAttributeAnalyzer(), AnalyzerIds.Sash0001MissingExtensionAttribute.Id },
+ { new Sash0002EventHandlerWithGenericParametersAnalyzer(), AnalyzerIds.Sash0002GenericEventHandlerUnsupported.Id },
+ { new Sash0003ApiStructMustBeReadonlyPartialAnalyzer(), AnalyzerIds.Sash0003ApiStructMustBeReadonlyPartial.Id },
+ { new Sash0004ApiStructMethodMarshalRefReturnNotSupportedAnalyzer(), AnalyzerIds.Sash0004ApiStructMarshalRefReturnUnsupported.Id },
+ { new Sash0005ApiStructRequiresAllowUnsafeBlocks(), AnalyzerIds.Sash0005ApiStructRequiresAllowUnsafeBlocks.Id },
+ { new Sash0006ApiStructBaseTypeMustBeApiStruct(), AnalyzerIds.Sash0006ApiStructBaseTypeMustBeApiStruct.Id },
+ { new Sash0007ApiStructMustNotContainFields(), AnalyzerIds.Sash0007ApiStructMustNotContainFields.Id },
+ { new Sash0008EventHandlerMarshalRefReturnUnsupported(), AnalyzerIds.Sash0008EventHandlerMarshalRefReturnUnsupported.Id },
+ };
+
+ [Theory]
+ [MemberData(nameof(Cases))]
+ public void SupportedDiagnostics_should_expose_exactly_one_descriptor_with_expected_id(DiagnosticAnalyzer analyzer, string expectedId)
+ {
+ analyzer.SupportedDiagnostics.Length.ShouldBe(1);
+ analyzer.SupportedDiagnostics[0].Id.ShouldBe(expectedId);
+ }
+}
+
+public class AnalyzerIdsTests
+{
+ [Fact]
+ public void AnalyzerIds_should_all_use_Correctness_category_and_Error_severity()
+ {
+ var descriptors = new[]
+ {
+ AnalyzerIds.Sash0001MissingExtensionAttribute,
+ AnalyzerIds.Sash0002GenericEventHandlerUnsupported,
+ AnalyzerIds.Sash0003ApiStructMustBeReadonlyPartial,
+ AnalyzerIds.Sash0004ApiStructMarshalRefReturnUnsupported,
+ AnalyzerIds.Sash0005ApiStructRequiresAllowUnsafeBlocks,
+ AnalyzerIds.Sash0006ApiStructBaseTypeMustBeApiStruct,
+ AnalyzerIds.Sash0007ApiStructMustNotContainFields,
+ AnalyzerIds.Sash0008EventHandlerMarshalRefReturnUnsupported,
+ };
+
+ foreach (var d in descriptors)
+ {
+ d.Category.ShouldBe(DiagnosticCategories.Correctness);
+ d.DefaultSeverity.ShouldBe(Microsoft.CodeAnalysis.DiagnosticSeverity.Error);
+ d.IsEnabledByDefault.ShouldBeTrue();
+ d.Id.ShouldStartWith("SASH");
+ }
+ }
+
+ [Fact]
+ public void AnalyzerIds_should_be_unique()
+ {
+ var ids = new[]
+ {
+ AnalyzerIds.Sash0001MissingExtensionAttribute.Id,
+ AnalyzerIds.Sash0002GenericEventHandlerUnsupported.Id,
+ AnalyzerIds.Sash0003ApiStructMustBeReadonlyPartial.Id,
+ AnalyzerIds.Sash0004ApiStructMarshalRefReturnUnsupported.Id,
+ AnalyzerIds.Sash0005ApiStructRequiresAllowUnsafeBlocks.Id,
+ AnalyzerIds.Sash0006ApiStructBaseTypeMustBeApiStruct.Id,
+ AnalyzerIds.Sash0007ApiStructMustNotContainFields.Id,
+ AnalyzerIds.Sash0008EventHandlerMarshalRefReturnUnsupported.Id,
+ };
+
+ ids.Distinct().Count().ShouldBe(ids.Length);
+ }
+}
diff --git a/test/SampSharp.CodeFixes.Tests/CodeFixTestHelper.cs b/test/SampSharp.CodeFixes.Tests/CodeFixTestHelper.cs
new file mode 100644
index 00000000..df04bd1a
--- /dev/null
+++ b/test/SampSharp.CodeFixes.Tests/CodeFixTestHelper.cs
@@ -0,0 +1,146 @@
+using System.Collections.Immutable;
+using System.Reflection;
+using System.Runtime.InteropServices;
+using System.Runtime.InteropServices.Marshalling;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CodeActions;
+using Microsoft.CodeAnalysis.CodeFixes;
+using Microsoft.CodeAnalysis.CSharp;
+using Microsoft.CodeAnalysis.Diagnostics;
+using Microsoft.CodeAnalysis.Host.Mef;
+using SampSharp.OpenMp.Core;
+
+namespace SampSharp.CodeFixes.Tests;
+
+///
+/// Drives an analyzer + code fix against a snippet, applies the first code action,
+/// and returns the resulting document text.
+///
+internal static class CodeFixTestHelper
+{
+ private static readonly Lazy> _references = new(BuildReferences);
+
+ public static async Task ApplyFixAsync(
+ DiagnosticAnalyzer analyzer,
+ CodeFixProvider codeFix,
+ string source,
+ bool allowUnsafe = true)
+ {
+ var workspace = new AdhocWorkspace(MefHostServices.DefaultHost);
+ var projectId = ProjectId.CreateNewId();
+ var documentId = DocumentId.CreateNewId(projectId);
+
+ var compilationOptions = new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary, allowUnsafe: allowUnsafe);
+
+ var solution = workspace.CurrentSolution
+ .AddProject(projectId, "TestProject", "TestProject", LanguageNames.CSharp)
+ .WithProjectCompilationOptions(projectId, compilationOptions)
+ .AddMetadataReferences(projectId, _references.Value)
+ .AddDocument(documentId, "Test.cs", source);
+
+ var document = solution.GetDocument(documentId)!;
+ var compilation = (await document.Project.GetCompilationAsync().ConfigureAwait(false))!;
+ var compilationWithAnalyzers = compilation.WithAnalyzers(ImmutableArray.Create(analyzer));
+ var diagnostics = await compilationWithAnalyzers.GetAnalyzerDiagnosticsAsync().ConfigureAwait(false);
+
+ var fixable = diagnostics.FirstOrDefault(d => codeFix.FixableDiagnosticIds.Contains(d.Id));
+ if (fixable == null)
+ {
+ return source;
+ }
+
+ var actions = new List();
+ var context = new CodeFixContext(document, fixable, (a, _) => actions.Add(a), CancellationToken.None);
+ await codeFix.RegisterCodeFixesAsync(context).ConfigureAwait(false);
+
+ if (actions.Count == 0)
+ {
+ return source;
+ }
+
+ var operations = await actions[0].GetOperationsAsync(CancellationToken.None).ConfigureAwait(false);
+ var solutionAfter = operations.OfType().Single().ChangedSolution;
+ var docAfter = solutionAfter.GetDocument(documentId)!;
+ var text = await docAfter.GetTextAsync().ConfigureAwait(false);
+
+ return text.ToString();
+ }
+
+ public static async Task ApplyFixAndGetCompilationOptionsAsync(
+ DiagnosticAnalyzer analyzer,
+ CodeFixProvider codeFix,
+ string source,
+ bool allowUnsafe = false)
+ {
+ var workspace = new AdhocWorkspace(MefHostServices.DefaultHost);
+ var projectId = ProjectId.CreateNewId();
+ var documentId = DocumentId.CreateNewId(projectId);
+
+ var compilationOptions = new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary, allowUnsafe: allowUnsafe);
+
+ var solution = workspace.CurrentSolution
+ .AddProject(projectId, "TestProject", "TestProject", LanguageNames.CSharp)
+ .WithProjectCompilationOptions(projectId, compilationOptions)
+ .AddMetadataReferences(projectId, _references.Value)
+ .AddDocument(documentId, "Test.cs", source);
+
+ var document = solution.GetDocument(documentId)!;
+ var compilation = (await document.Project.GetCompilationAsync().ConfigureAwait(false))!;
+ var compilationWithAnalyzers = compilation.WithAnalyzers(ImmutableArray.Create(analyzer));
+ var diagnostics = await compilationWithAnalyzers.GetAnalyzerDiagnosticsAsync().ConfigureAwait(false);
+
+ var fixable = diagnostics.FirstOrDefault(d => codeFix.FixableDiagnosticIds.Contains(d.Id));
+ if (fixable == null)
+ {
+ return null;
+ }
+
+ var actions = new List();
+ var context = new CodeFixContext(document, fixable, (a, _) => actions.Add(a), CancellationToken.None);
+ await codeFix.RegisterCodeFixesAsync(context).ConfigureAwait(false);
+
+ if (actions.Count == 0)
+ {
+ return null;
+ }
+
+ var operations = await actions[0].GetOperationsAsync(CancellationToken.None).ConfigureAwait(false);
+ var solutionAfter = operations.OfType().Single().ChangedSolution;
+ var projectAfter = solutionAfter.GetProject(projectId)!;
+
+ return projectAfter.CompilationOptions as CSharpCompilationOptions;
+ }
+
+ private static IReadOnlyList BuildReferences()
+ {
+ var runtimeDir = Path.GetDirectoryName(typeof(object).Assembly.Location)!;
+ var refs = new List();
+
+ void Add(Assembly assembly)
+ {
+ if (!string.IsNullOrEmpty(assembly.Location) && File.Exists(assembly.Location))
+ {
+ refs.Add(MetadataReference.CreateFromFile(assembly.Location));
+ }
+ }
+
+ Add(typeof(object).Assembly);
+ Add(typeof(Attribute).Assembly);
+ Add(typeof(Console).Assembly);
+ Add(typeof(Marshal).Assembly);
+ Add(typeof(CustomMarshallerAttribute).Assembly);
+
+ foreach (var name in new[] { "System.Runtime.dll", "System.Collections.dll", "netstandard.dll" })
+ {
+ var path = Path.Combine(runtimeDir, name);
+ if (File.Exists(path))
+ {
+ refs.Add(MetadataReference.CreateFromFile(path));
+ }
+ }
+
+ refs.Add(MetadataReference.CreateFromFile(typeof(OpenMpApiAttribute).Assembly.Location));
+
+ return refs;
+ }
+}
diff --git a/test/SampSharp.CodeFixes.Tests/SampSharp.CodeFixes.Tests.csproj b/test/SampSharp.CodeFixes.Tests/SampSharp.CodeFixes.Tests.csproj
new file mode 100644
index 00000000..f9fcdde2
--- /dev/null
+++ b/test/SampSharp.CodeFixes.Tests/SampSharp.CodeFixes.Tests.csproj
@@ -0,0 +1,25 @@
+
+
+
+ Exe
+
+
+
+
+
+
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+
+
+
+
+
+
+
+
diff --git a/test/SampSharp.CodeFixes.Tests/Sash0001ExtensionAttributeCodeFixProviderTests.cs b/test/SampSharp.CodeFixes.Tests/Sash0001ExtensionAttributeCodeFixProviderTests.cs
new file mode 100644
index 00000000..b4481d50
--- /dev/null
+++ b/test/SampSharp.CodeFixes.Tests/Sash0001ExtensionAttributeCodeFixProviderTests.cs
@@ -0,0 +1,75 @@
+using SampSharp.Analyzer;
+using SampSharp.Analyzer.Analyzers;
+using Shouldly;
+using Xunit;
+
+namespace SampSharp.CodeFixes.Tests;
+
+public class Sash0001ExtensionAttributeCodeFixProviderTests
+{
+ [Fact]
+ public void FixableDiagnosticIds_should_contain_only_Sash0001()
+ {
+ var provider = new Sash0001ExtensionAttributeCodeFixProvider();
+ provider.FixableDiagnosticIds.ShouldBe(new[] { AnalyzerIds.Sash0001MissingExtensionAttribute.Id });
+ }
+
+ [Fact]
+ public async Task Fix_should_add_ExtensionAttribute_to_class_extending_Extension()
+ {
+ const string source = """
+ using SampSharp.OpenMp.Core;
+
+ public class MyExt : Extension { }
+ """;
+
+ var result = await CodeFixTestHelper.ApplyFixAsync(
+ new Sash0001ExtensionAttributeAnalyzer(),
+ new Sash0001ExtensionAttributeCodeFixProvider(),
+ source);
+
+ result.ShouldContain("[Extension(0x");
+ result.ShouldContain("public class MyExt : Extension");
+ }
+
+ [Fact]
+ public async Task Fix_should_add_using_for_Core_namespace_when_missing()
+ {
+ const string source = """
+ public class MyExt : SampSharp.OpenMp.Core.Extension { }
+ """;
+
+ var result = await CodeFixTestHelper.ApplyFixAsync(
+ new Sash0001ExtensionAttributeAnalyzer(),
+ new Sash0001ExtensionAttributeCodeFixProvider(),
+ source);
+
+ result.ShouldContain("using SampSharp.OpenMp.Core;");
+ result.ShouldContain("[Extension(");
+ }
+
+ [Fact]
+ public async Task Fix_should_not_add_duplicate_using()
+ {
+ const string source = """
+ using SampSharp.OpenMp.Core;
+
+ public class MyExt : Extension { }
+ """;
+
+ var result = await CodeFixTestHelper.ApplyFixAsync(
+ new Sash0001ExtensionAttributeAnalyzer(),
+ new Sash0001ExtensionAttributeCodeFixProvider(),
+ source);
+
+ var occurrences = result.Split("using SampSharp.OpenMp.Core;").Length - 1;
+ occurrences.ShouldBe(1);
+ }
+
+ [Fact]
+ public void GetFixAllProvider_should_return_non_null_batch_fixer()
+ {
+ var provider = new Sash0001ExtensionAttributeCodeFixProvider();
+ provider.GetFixAllProvider().ShouldNotBeNull();
+ }
+}
diff --git a/test/SampSharp.CodeFixes.Tests/Sash0003MakeStructPartialCodeFixProviderTests.cs b/test/SampSharp.CodeFixes.Tests/Sash0003MakeStructPartialCodeFixProviderTests.cs
new file mode 100644
index 00000000..6ce8551c
--- /dev/null
+++ b/test/SampSharp.CodeFixes.Tests/Sash0003MakeStructPartialCodeFixProviderTests.cs
@@ -0,0 +1,79 @@
+using SampSharp.Analyzer;
+using SampSharp.Analyzer.Analyzers;
+using Shouldly;
+using Xunit;
+
+namespace SampSharp.CodeFixes.Tests;
+
+public class Sash0003MakeStructPartialCodeFixProviderTests
+{
+ [Fact]
+ public void FixableDiagnosticIds_should_contain_only_Sash0003()
+ {
+ var provider = new Sash0003MakeStructPartialCodeFixProvider();
+ provider.FixableDiagnosticIds.ShouldBe(new[] { AnalyzerIds.Sash0003ApiStructMustBeReadonlyPartial.Id });
+ }
+
+ [Fact]
+ public async Task Fix_should_add_partial_when_missing()
+ {
+ const string source = """
+ using SampSharp.OpenMp.Core;
+
+ [OpenMpApi]
+ public readonly struct MyApi { }
+ """;
+
+ var result = await CodeFixTestHelper.ApplyFixAsync(
+ new Sash0003ApiStructMustBeReadonlyPartialAnalyzer(),
+ new Sash0003MakeStructPartialCodeFixProvider(),
+ source);
+
+ result.ShouldContain("readonly");
+ result.ShouldContain("partial struct MyApi");
+ }
+
+ [Fact]
+ public async Task Fix_should_add_readonly_when_missing()
+ {
+ const string source = """
+ using SampSharp.OpenMp.Core;
+
+ [OpenMpApi]
+ public partial struct MyApi { }
+ """;
+
+ var result = await CodeFixTestHelper.ApplyFixAsync(
+ new Sash0003ApiStructMustBeReadonlyPartialAnalyzer(),
+ new Sash0003MakeStructPartialCodeFixProvider(),
+ source);
+
+ result.ShouldContain("readonly");
+ result.ShouldContain("partial struct MyApi");
+ }
+
+ [Fact]
+ public async Task Fix_should_add_both_when_both_missing()
+ {
+ const string source = """
+ using SampSharp.OpenMp.Core;
+
+ [OpenMpApi]
+ public struct MyApi { }
+ """;
+
+ var result = await CodeFixTestHelper.ApplyFixAsync(
+ new Sash0003ApiStructMustBeReadonlyPartialAnalyzer(),
+ new Sash0003MakeStructPartialCodeFixProvider(),
+ source);
+
+ result.ShouldContain("readonly");
+ result.ShouldContain("partial struct MyApi");
+ }
+
+ [Fact]
+ public void GetFixAllProvider_should_return_non_null_batch_fixer()
+ {
+ new Sash0003MakeStructPartialCodeFixProvider().GetFixAllProvider().ShouldNotBeNull();
+ }
+}
diff --git a/test/SampSharp.CodeFixes.Tests/Sash0005AllowUnsafeBlocksCodeFixProviderTests.cs b/test/SampSharp.CodeFixes.Tests/Sash0005AllowUnsafeBlocksCodeFixProviderTests.cs
new file mode 100644
index 00000000..7d1a035d
--- /dev/null
+++ b/test/SampSharp.CodeFixes.Tests/Sash0005AllowUnsafeBlocksCodeFixProviderTests.cs
@@ -0,0 +1,42 @@
+using SampSharp.Analyzer;
+using SampSharp.Analyzer.Analyzers;
+using Shouldly;
+using Xunit;
+
+namespace SampSharp.CodeFixes.Tests;
+
+public class Sash0005AllowUnsafeBlocksCodeFixProviderTests
+{
+ [Fact]
+ public void FixableDiagnosticIds_should_contain_only_Sash0005()
+ {
+ var provider = new Sash0005AllowUnsafeBlocksCodeFixProvider();
+ provider.FixableDiagnosticIds.ShouldBe(new[] { AnalyzerIds.Sash0005ApiStructRequiresAllowUnsafeBlocks.Id });
+ }
+
+ [Fact]
+ public async Task Fix_should_enable_AllowUnsafe_on_project_compilation_options()
+ {
+ const string source = """
+ using SampSharp.OpenMp.Core;
+
+ [OpenMpApi]
+ public readonly partial struct MyApi { }
+ """;
+
+ var options = await CodeFixTestHelper.ApplyFixAndGetCompilationOptionsAsync(
+ new Sash0005ApiStructRequiresAllowUnsafeBlocks(),
+ new Sash0005AllowUnsafeBlocksCodeFixProvider(),
+ source,
+ allowUnsafe: false);
+
+ options.ShouldNotBeNull();
+ options!.AllowUnsafe.ShouldBeTrue();
+ }
+
+ [Fact]
+ public void GetFixAllProvider_should_return_non_null_batch_fixer()
+ {
+ new Sash0005AllowUnsafeBlocksCodeFixProvider().GetFixAllProvider().ShouldNotBeNull();
+ }
+}
diff --git a/test/SampSharp.OpenMp.Core.Tests/ChronoDurationTests.cs b/test/SampSharp.OpenMp.Core.Tests/ChronoDurationTests.cs
new file mode 100644
index 00000000..10d20130
--- /dev/null
+++ b/test/SampSharp.OpenMp.Core.Tests/ChronoDurationTests.cs
@@ -0,0 +1,148 @@
+using SampSharp.OpenMp.Core.Std.Chrono;
+using Shouldly;
+using Xunit;
+
+namespace SampSharp.OpenMp.Core.Tests;
+
+public class ChronoDurationTests
+{
+ [Fact]
+ public void Seconds_AsTimeSpan_should_return_equivalent_TimeSpan()
+ {
+ var s = new Seconds(42);
+ s.AsTimeSpan().ShouldBe(TimeSpan.FromSeconds(42));
+ }
+
+ [Fact]
+ public void Seconds_should_implicitly_convert_to_TimeSpan()
+ {
+ TimeSpan ts = new Seconds(10);
+ ts.ShouldBe(TimeSpan.FromSeconds(10));
+ }
+
+ [Fact]
+ public void Seconds_should_implicitly_convert_from_TimeSpan()
+ {
+ Seconds s = TimeSpan.FromSeconds(7);
+ ((TimeSpan)s).ShouldBe(TimeSpan.FromSeconds(7));
+ }
+
+ [Fact]
+ public void Seconds_implicit_from_TimeSpan_should_truncate_sub_second_remainder()
+ {
+ Seconds s = TimeSpan.FromMilliseconds(1500);
+ ((TimeSpan)s).ShouldBe(TimeSpan.FromSeconds(1));
+ }
+
+ [Fact]
+ public void Seconds_ToString_should_use_invariant_culture()
+ {
+ new Seconds(123).ToString().ShouldBe("123");
+ }
+
+ [Fact]
+ public void Milliseconds_AsTimeSpan_should_return_equivalent_TimeSpan()
+ {
+ new Milliseconds(500).AsTimeSpan().ShouldBe(TimeSpan.FromMilliseconds(500));
+ }
+
+ [Fact]
+ public void Milliseconds_should_roundtrip_via_TimeSpan()
+ {
+ var original = new Milliseconds(750);
+ TimeSpan ts = original;
+ Milliseconds back = ts;
+ ((TimeSpan)back).ShouldBe((TimeSpan)original);
+ }
+
+ [Fact]
+ public void Milliseconds_implicit_from_TimeSpan_should_truncate_sub_millisecond_remainder()
+ {
+ Milliseconds ms = TimeSpan.FromTicks(TimeSpan.TicksPerMillisecond + 5);
+ ((TimeSpan)ms).ShouldBe(TimeSpan.FromMilliseconds(1));
+ }
+
+ [Fact]
+ public void Milliseconds_ToString_should_use_invariant_culture()
+ {
+ new Milliseconds(987).ToString().ShouldBe("987");
+ }
+
+ [Fact]
+ public void Microseconds_AsTimeSpan_should_return_equivalent_TimeSpan()
+ {
+ new Microseconds(2000).AsTimeSpan().ShouldBe(TimeSpan.FromMicroseconds(2000));
+ }
+
+ [Fact]
+ public void Microseconds_should_implicitly_convert_to_TimeSpan()
+ {
+ TimeSpan ts = new Microseconds(1000);
+ ts.ShouldBe(TimeSpan.FromMicroseconds(1000));
+ }
+
+ [Fact]
+ public void Microseconds_should_implicitly_convert_from_TimeSpan()
+ {
+ Microseconds us = TimeSpan.FromMicroseconds(500);
+ ((TimeSpan)us).ShouldBe(TimeSpan.FromMicroseconds(500));
+ }
+
+ [Fact]
+ public void Microseconds_ToString_should_use_invariant_culture()
+ {
+ new Microseconds(54321).ToString().ShouldBe("54321");
+ }
+
+ [Fact]
+ public void Minutes_AsTimeSpan_should_return_equivalent_TimeSpan()
+ {
+ new Minutes(5).AsTimeSpan().ShouldBe(TimeSpan.FromMinutes(5));
+ }
+
+ [Fact]
+ public void Minutes_should_implicitly_convert_to_TimeSpan()
+ {
+ TimeSpan ts = new Minutes(15);
+ ts.ShouldBe(TimeSpan.FromMinutes(15));
+ }
+
+ [Fact]
+ public void Minutes_implicit_from_TimeSpan_should_truncate_sub_minute_remainder()
+ {
+ Minutes m = TimeSpan.FromSeconds(125);
+ ((TimeSpan)m).ShouldBe(TimeSpan.FromMinutes(2));
+ }
+
+ [Fact]
+ public void Minutes_ToString_should_use_invariant_culture()
+ {
+ new Minutes(42).ToString().ShouldBe("42");
+ }
+
+ [Fact]
+ public void Hours_AsTimeSpan_should_return_equivalent_TimeSpan()
+ {
+ new Hours(3).AsTimeSpan().ShouldBe(TimeSpan.FromHours(3));
+ }
+
+ [Fact]
+ public void Hours_should_implicitly_convert_to_TimeSpan()
+ {
+ TimeSpan ts = new Hours(24);
+ ts.ShouldBe(TimeSpan.FromHours(24));
+ }
+
+ [Fact]
+ public void Hours_implicit_from_TimeSpan_should_truncate_sub_hour_remainder()
+ {
+ Hours h = TimeSpan.FromMinutes(125);
+ ((TimeSpan)h).ShouldBe(TimeSpan.FromHours(2));
+ }
+
+ [Fact]
+ public void Hours_ToString_should_use_invariant_culture()
+ {
+ new Hours(99).ToString().ShouldBe("99");
+ }
+}
diff --git a/test/SampSharp.OpenMp.Core.Tests/PairTests.cs b/test/SampSharp.OpenMp.Core.Tests/PairTests.cs
new file mode 100644
index 00000000..274fed7e
--- /dev/null
+++ b/test/SampSharp.OpenMp.Core.Tests/PairTests.cs
@@ -0,0 +1,50 @@
+using SampSharp.OpenMp.Core.Std;
+using Shouldly;
+using Xunit;
+
+namespace SampSharp.OpenMp.Core.Tests;
+
+public class PairTests
+{
+ [Fact]
+ public void Implicit_from_tuple_should_set_components()
+ {
+ Pair pair = (10, 20);
+ pair.First.ShouldBe(10);
+ pair.Second.ShouldBe(20);
+ }
+
+ [Fact]
+ public void Implicit_to_tuple_should_return_components()
+ {
+ Pair pair = (3, 4);
+ (int a, int b) = pair;
+ a.ShouldBe(3);
+ b.ShouldBe(4);
+ }
+
+ [Fact]
+ public void Deconstruct_should_return_components()
+ {
+ Pair pair = (5, 99L);
+ pair.Deconstruct(out var first, out var second);
+ first.ShouldBe(5);
+ second.ShouldBe(99L);
+ }
+
+ [Fact]
+ public void ToString_should_format_as_parenthesized_pair()
+ {
+ Pair pair = (1, 2);
+ pair.ToString().ShouldBe("(1, 2)");
+ }
+
+ [Fact]
+ public void Implicit_tuple_conversion_should_preserve_mixed_types()
+ {
+ Pair pair = ((byte)7, 12345L);
+ ValueTuple t = pair;
+ t.Item1.ShouldBe((byte)7);
+ t.Item2.ShouldBe(12345L);
+ }
+}
diff --git a/test/SampSharp.OpenMp.Core.Tests/SizeTests.cs b/test/SampSharp.OpenMp.Core.Tests/SizeTests.cs
new file mode 100644
index 00000000..98418eba
--- /dev/null
+++ b/test/SampSharp.OpenMp.Core.Tests/SizeTests.cs
@@ -0,0 +1,42 @@
+using SampSharp.OpenMp.Core.Std;
+using Shouldly;
+using Xunit;
+
+namespace SampSharp.OpenMp.Core.Tests;
+
+public class SizeTests
+{
+ [Fact]
+ public void Ctor_should_set_Value()
+ {
+ var s = new Size(42);
+ s.Value.ShouldBe((nint)42);
+ }
+
+ [Fact]
+ public void ToInt32_should_convert_value()
+ {
+ var s = new Size(123);
+ s.ToInt32().ShouldBe(123);
+ }
+
+ [Fact]
+ public void Explicit_operator_should_convert_to_int()
+ {
+ var s = new Size(500);
+ ((int)s).ShouldBe(500);
+ }
+
+ [Fact]
+ public void Implicit_operator_should_convert_from_int()
+ {
+ Size s = 99;
+ s.Value.ShouldBe((nint)99);
+ }
+
+ [Fact]
+ public void Length_should_be_8_bytes()
+ {
+ Size.Length.ShouldBe(8);
+ }
+}
diff --git a/test/SampSharp.OpenMp.Entities.Tests/ColorTests.cs b/test/SampSharp.OpenMp.Entities.Tests/ColorTests.cs
new file mode 100644
index 00000000..8ef42675
--- /dev/null
+++ b/test/SampSharp.OpenMp.Entities.Tests/ColorTests.cs
@@ -0,0 +1,576 @@
+using System.Numerics;
+using SampSharp.Entities.SAMP;
+using SampSharp.OpenMp.Core.Api;
+using Shouldly;
+using Xunit;
+
+namespace SampSharp.OpenMp.Entities.Tests;
+
+public class ColorTests
+{
+ [Fact]
+ public void Ctor_byte_rgba_should_set_components()
+ {
+ var c = new Color((byte)10, (byte)20, (byte)30, (byte)40);
+ c.R.ShouldBe((byte)10);
+ c.G.ShouldBe((byte)20);
+ c.B.ShouldBe((byte)30);
+ c.A.ShouldBe((byte)40);
+ }
+
+ [Fact]
+ public void Ctor_byte_rgb_should_default_alpha_to_255()
+ {
+ var c = new Color((byte)1, (byte)2, (byte)3);
+ c.A.ShouldBe((byte)255);
+ }
+
+ [Fact]
+ public void Ctor_byte_rgb_with_float_alpha_should_scale_to_byte()
+ {
+ var c = new Color((byte)1, (byte)2, (byte)3, 0.5f);
+ c.A.ShouldBe((byte)127);
+ }
+
+ [Fact]
+ public void Ctor_byte_rgb_with_float_alpha_should_clamp_above_one()
+ {
+ var c = new Color((byte)1, (byte)2, (byte)3, 5f);
+ c.A.ShouldBe((byte)255);
+ }
+
+ [Fact]
+ public void Ctor_byte_rgb_with_float_alpha_should_clamp_below_zero()
+ {
+ var c = new Color((byte)1, (byte)2, (byte)3, -1f);
+ c.A.ShouldBe((byte)0);
+ }
+
+ [Fact]
+ public void Ctor_int_rgba_should_clamp_above_255()
+ {
+ var c = new Color(300, 400, 500, 600);
+ c.R.ShouldBe((byte)255);
+ c.G.ShouldBe((byte)255);
+ c.B.ShouldBe((byte)255);
+ c.A.ShouldBe((byte)255);
+ }
+
+ [Fact]
+ public void Ctor_int_rgba_should_clamp_below_zero()
+ {
+ var c = new Color(-10, -20, -30, -40);
+ c.R.ShouldBe((byte)0);
+ c.G.ShouldBe((byte)0);
+ c.B.ShouldBe((byte)0);
+ c.A.ShouldBe((byte)0);
+ }
+
+ [Fact]
+ public void Ctor_int_rgb_should_default_alpha_to_255()
+ {
+ var c = new Color(10, 20, 30);
+ c.A.ShouldBe((byte)255);
+ }
+
+ [Fact]
+ public void Ctor_float_rgba_should_scale_to_byte_range()
+ {
+ var c = new Color(1.0f, 0.5f, 0.0f, 1.0f);
+ c.R.ShouldBe((byte)255);
+ c.G.ShouldBe((byte)127);
+ c.B.ShouldBe((byte)0);
+ c.A.ShouldBe((byte)255);
+ }
+
+ [Fact]
+ public void Ctor_float_rgba_should_clamp_out_of_range_values()
+ {
+ var c = new Color(2.0f, -1.0f, 1.5f, -0.5f);
+ c.R.ShouldBe((byte)255);
+ c.G.ShouldBe((byte)0);
+ c.B.ShouldBe((byte)255);
+ c.A.ShouldBe((byte)0);
+ }
+
+ [Fact]
+ public void Ctor_float_rgb_should_default_alpha_to_one()
+ {
+ var c = new Color(0.5f, 0.5f, 0.5f);
+ c.A.ShouldBe((byte)255);
+ }
+
+ [Fact]
+ public void Ctor_int_packed_should_unpack_as_RGBA()
+ {
+ // 0xAABBCCDD with RGBA => R=AA G=BB B=CC A=DD
+ var c = new Color(unchecked((int)0xAABBCCDD));
+ c.R.ShouldBe((byte)0xAA);
+ c.G.ShouldBe((byte)0xBB);
+ c.B.ShouldBe((byte)0xCC);
+ c.A.ShouldBe((byte)0xDD);
+ }
+
+ [Fact]
+ public void Ctor_uint_packed_should_unpack_as_RGBA()
+ {
+ var c = new Color(0xAABBCCDDu);
+ c.R.ShouldBe((byte)0xAA);
+ c.G.ShouldBe((byte)0xBB);
+ c.B.ShouldBe((byte)0xCC);
+ c.A.ShouldBe((byte)0xDD);
+ }
+
+ [Fact]
+ public void Brightness_should_be_computed_correctly()
+ {
+ var c = new Color((byte)100, (byte)150, (byte)200);
+ c.Brightness.ShouldBe(0.212655f * 100 + 0.715158f * 150 + 0.072187f * 200, 0.0001f);
+ }
+
+ [Fact]
+ public void Brightness_for_white_should_be_close_to_max()
+ {
+ var c = Color.White;
+ c.Brightness.ShouldBe(0.212655f * 255 + 0.715158f * 255 + 0.072187f * 255, 0.0001f);
+ }
+
+ [Fact]
+ public void ToInteger_RGBA_should_pack_components_correctly()
+ {
+ var c = new Color((byte)0xAA, (byte)0xBB, (byte)0xCC, (byte)0xDD);
+ unchecked
+ {
+ c.ToInteger(ColorFormat.RGBA).ShouldBe((int)0xAABBCCDD);
+ }
+ }
+
+ [Fact]
+ public void ToInteger_ARGB_should_pack_components_correctly()
+ {
+ var c = new Color((byte)0xAA, (byte)0xBB, (byte)0xCC, (byte)0xDD);
+ unchecked
+ {
+ c.ToInteger(ColorFormat.ARGB).ShouldBe((int)0xDDAABBCC);
+ }
+ }
+
+ [Fact]
+ public void ToInteger_RGB_should_pack_components_correctly()
+ {
+ var c = new Color((byte)0xAA, (byte)0xBB, (byte)0xCC, (byte)0xDD);
+ c.ToInteger(ColorFormat.RGB).ShouldBe(0xAABBCC);
+ }
+
+ [Fact]
+ public void ToInteger_should_return_zero_for_unknown_format()
+ {
+ var c = new Color((byte)1, (byte)2, (byte)3, (byte)4);
+ c.ToInteger((ColorFormat)99).ShouldBe(0);
+ }
+
+ [Fact]
+ public void FromInteger_RGBA_should_unpack_correctly()
+ {
+ var c = Color.FromInteger(0xAABBCCDDu, ColorFormat.RGBA);
+ c.R.ShouldBe((byte)0xAA);
+ c.G.ShouldBe((byte)0xBB);
+ c.B.ShouldBe((byte)0xCC);
+ c.A.ShouldBe((byte)0xDD);
+ }
+
+ [Fact]
+ public void FromInteger_ARGB_should_unpack_correctly()
+ {
+ var c = Color.FromInteger(0xDDAABBCCu, ColorFormat.ARGB);
+ c.A.ShouldBe((byte)0xDD);
+ c.R.ShouldBe((byte)0xAA);
+ c.G.ShouldBe((byte)0xBB);
+ c.B.ShouldBe((byte)0xCC);
+ }
+
+ [Fact]
+ public void FromInteger_RGB_should_unpack_and_set_full_alpha()
+ {
+ var c = Color.FromInteger(0xAABBCCu, ColorFormat.RGB);
+ c.R.ShouldBe((byte)0xAA);
+ c.G.ShouldBe((byte)0xBB);
+ c.B.ShouldBe((byte)0xCC);
+ c.A.ShouldBe((byte)0xFF);
+ }
+
+ [Fact]
+ public void FromInteger_signed_overload_should_pass_through_to_unsigned()
+ {
+ var u = Color.FromInteger(0xAABBCCDDu, ColorFormat.RGBA);
+ var s = Color.FromInteger(unchecked((int)0xAABBCCDD), ColorFormat.RGBA);
+ s.ShouldBe(u);
+ }
+
+ [Fact]
+ public void ToInteger_then_FromInteger_RGBA_should_roundtrip()
+ {
+ var c = new Color((byte)17, (byte)33, (byte)49, (byte)65);
+ var back = Color.FromInteger(unchecked((uint)c.ToInteger(ColorFormat.RGBA)), ColorFormat.RGBA);
+ back.ShouldBe(c);
+ }
+
+ [Fact]
+ public void ToInteger_then_FromInteger_ARGB_should_roundtrip()
+ {
+ var c = new Color((byte)17, (byte)33, (byte)49, (byte)65);
+ var back = Color.FromInteger(unchecked((uint)c.ToInteger(ColorFormat.ARGB)), ColorFormat.ARGB);
+ back.ShouldBe(c);
+ }
+
+ [Fact]
+ public void FromString_RGBA_should_parse_valid_8_hex_chars()
+ {
+ var c = Color.FromString("AABBCCDD", ColorFormat.RGBA);
+ c.R.ShouldBe((byte)0xAA);
+ c.G.ShouldBe((byte)0xBB);
+ c.B.ShouldBe((byte)0xCC);
+ c.A.ShouldBe((byte)0xDD);
+ }
+
+ [Fact]
+ public void FromString_RGB_should_parse_valid_6_hex_chars()
+ {
+ var c = Color.FromString("AABBCC", ColorFormat.RGB);
+ c.R.ShouldBe((byte)0xAA);
+ c.G.ShouldBe((byte)0xBB);
+ c.B.ShouldBe((byte)0xCC);
+ c.A.ShouldBe((byte)0xFF);
+ }
+
+ [Fact]
+ public void FromString_should_accept_0x_prefix()
+ {
+ var c = Color.FromString("0xAABBCC", ColorFormat.RGB);
+ c.R.ShouldBe((byte)0xAA);
+ }
+
+ [Fact]
+ public void FromString_should_return_white_for_invalid_input()
+ {
+ Color.FromString("not-a-color", ColorFormat.RGB).ShouldBe(Color.White);
+ }
+
+ [Fact]
+ public void FromString_should_return_white_when_length_does_not_match_format()
+ {
+ Color.FromString("AABBCC", ColorFormat.RGBA).ShouldBe(Color.White);
+ }
+
+ [Fact]
+ public void Lerp_at_zero_should_return_value1_rgb()
+ {
+ var a = new Color((byte)0, (byte)0, (byte)0, (byte)10);
+ var b = new Color((byte)100, (byte)100, (byte)100, (byte)200);
+ var result = Color.Lerp(a, b, 0f);
+ result.R.ShouldBe((byte)0);
+ result.G.ShouldBe((byte)0);
+ result.B.ShouldBe((byte)0);
+ result.A.ShouldBe((byte)10);
+ }
+
+ [Fact]
+ public void Lerp_at_one_should_return_value2_rgb_but_keep_value1_alpha_when_blendAlpha_is_false()
+ {
+ var a = new Color((byte)0, (byte)0, (byte)0, (byte)10);
+ var b = new Color((byte)100, (byte)100, (byte)100, (byte)200);
+ var result = Color.Lerp(a, b, 1f);
+ result.R.ShouldBe((byte)100);
+ result.G.ShouldBe((byte)100);
+ result.B.ShouldBe((byte)100);
+ result.A.ShouldBe((byte)10);
+ }
+
+ [Fact]
+ public void Lerp_at_one_with_blendAlpha_should_return_value2_alpha()
+ {
+ var a = new Color((byte)0, (byte)0, (byte)0, (byte)10);
+ var b = new Color((byte)100, (byte)100, (byte)100, (byte)200);
+ var result = Color.Lerp(a, b, 1f, blendAlpha: true);
+ result.A.ShouldBe((byte)200);
+ }
+
+ [Fact]
+ public void Lerp_should_clamp_amount_above_one()
+ {
+ var a = new Color((byte)0, (byte)0, (byte)0);
+ var b = new Color((byte)100, (byte)100, (byte)100);
+ Color.Lerp(a, b, 5f).R.ShouldBe((byte)100);
+ }
+
+ [Fact]
+ public void Lerp_should_clamp_amount_below_zero()
+ {
+ var a = new Color((byte)20, (byte)20, (byte)20);
+ var b = new Color((byte)100, (byte)100, (byte)100);
+ Color.Lerp(a, b, -1f).R.ShouldBe((byte)20);
+ }
+
+ [Fact]
+ public void Darken_at_full_amount_should_return_black_rgb()
+ {
+ var result = Color.White.Darken(1f);
+ result.R.ShouldBe((byte)0);
+ result.G.ShouldBe((byte)0);
+ result.B.ShouldBe((byte)0);
+ }
+
+ [Fact]
+ public void Lighten_at_full_amount_should_return_white_rgb()
+ {
+ var result = Color.Black.Lighten(1f);
+ result.R.ShouldBe((byte)255);
+ result.G.ShouldBe((byte)255);
+ result.B.ShouldBe((byte)255);
+ }
+
+ [Fact]
+ public void Grayscale_should_set_rgb_to_brightness()
+ {
+ var c = new Color((byte)100, (byte)150, (byte)200);
+ var gs = c.Grayscale();
+ gs.R.ShouldBe(gs.G);
+ gs.G.ShouldBe(gs.B);
+ }
+
+ [Fact]
+ public void AddGammaCorrection_then_RemoveGammaCorrection_should_roundtrip_close_to_original()
+ {
+ var c = new Color((byte)128, (byte)200, (byte)50, (byte)255);
+ var roundtrip = c.AddGammaCorrection().RemoveGammaCorrection();
+ ((int)roundtrip.R).ShouldBeInRange(126, 130);
+ ((int)roundtrip.G).ShouldBeInRange(198, 202);
+ ((int)roundtrip.B).ShouldBeInRange(48, 52);
+ }
+
+ [Fact]
+ public void AddGammaCorrection_should_keep_white_close_to_white()
+ {
+ var c = Color.White.AddGammaCorrection();
+ ((int)c.R).ShouldBeGreaterThanOrEqualTo(254);
+ ((int)c.G).ShouldBeGreaterThanOrEqualTo(254);
+ ((int)c.B).ShouldBeGreaterThanOrEqualTo(254);
+ }
+
+ [Fact]
+ public void RemoveGammaCorrection_should_keep_black_as_black()
+ {
+ Color.Black.RemoveGammaCorrection().ShouldBe(Color.Black);
+ }
+
+ [Fact]
+ public void ToString_should_use_RGB_format_by_default()
+ {
+ var c = new Color((byte)0xAA, (byte)0xBB, (byte)0xCC, (byte)0xDD);
+ c.ToString().ShouldBe("{AABBCC}");
+ }
+
+ [Fact]
+ public void ToString_RGBA_should_format_as_curly_8_hex()
+ {
+ var c = new Color((byte)0xAA, (byte)0xBB, (byte)0xCC, (byte)0xDD);
+ c.ToString(ColorFormat.RGBA).ShouldBe("{AABBCCDD}");
+ }
+
+ [Fact]
+ public void ToString_ARGB_should_format_as_curly_8_hex()
+ {
+ var c = new Color((byte)0xAA, (byte)0xBB, (byte)0xCC, (byte)0xDD);
+ c.ToString(ColorFormat.ARGB).ShouldBe("{DDAABBCC}");
+ }
+
+ [Fact]
+ public void Implicit_to_int_should_use_RGBA_format()
+ {
+ var c = new Color((byte)0x11, (byte)0x22, (byte)0x33, (byte)0x44);
+ int value = c;
+ value.ShouldBe(c.ToInteger(ColorFormat.RGBA));
+ }
+
+ [Fact]
+ public void Implicit_int_to_Color_should_use_RGBA_format()
+ {
+ Color c = unchecked((int)0xAABBCCDDu);
+ c.R.ShouldBe((byte)0xAA);
+ c.A.ShouldBe((byte)0xDD);
+ }
+
+ [Fact]
+ public void Implicit_uint_to_Color_should_use_RGBA_format()
+ {
+ Color c = 0xAABBCCDDu;
+ c.R.ShouldBe((byte)0xAA);
+ c.A.ShouldBe((byte)0xDD);
+ }
+
+ [Fact]
+ public void Implicit_Colour_to_Color_should_copy_components()
+ {
+ var src = new Colour(10, 20, 30, 40);
+ Color c = src;
+ c.R.ShouldBe((byte)10);
+ c.G.ShouldBe((byte)20);
+ c.B.ShouldBe((byte)30);
+ c.A.ShouldBe((byte)40);
+ }
+
+ [Fact]
+ public void Implicit_Color_to_Colour_should_copy_components()
+ {
+ var src = new Color((byte)10, (byte)20, (byte)30, (byte)40);
+ Colour c = src;
+ c.R.ShouldBe((byte)10);
+ c.G.ShouldBe((byte)20);
+ c.B.ShouldBe((byte)30);
+ c.A.ShouldBe((byte)40);
+ }
+
+ [Fact]
+ public void EqualityOperator_should_return_true_for_same_components()
+ {
+ var a = new Color((byte)1, (byte)2, (byte)3, (byte)4);
+ var b = new Color((byte)1, (byte)2, (byte)3, (byte)4);
+ (a == b).ShouldBeTrue();
+ }
+
+ [Fact]
+ public void EqualityOperator_should_return_false_for_differing_components()
+ {
+ var a = new Color((byte)1, (byte)2, (byte)3, (byte)4);
+ var b = new Color((byte)9, (byte)2, (byte)3, (byte)4);
+ (a == b).ShouldBeFalse();
+ }
+
+ [Fact]
+ public void InequalityOperator_should_invert_equality()
+ {
+ var a = new Color((byte)1, (byte)2, (byte)3, (byte)4);
+ var b = new Color((byte)9, (byte)2, (byte)3, (byte)4);
+ (a != b).ShouldBeTrue();
+ }
+
+ [Fact]
+ public void MultiplyOperator_should_scale_components()
+ {
+ var c = new Color((byte)100, (byte)100, (byte)100, (byte)100) * 0.5f;
+ c.R.ShouldBe((byte)50);
+ c.G.ShouldBe((byte)50);
+ c.B.ShouldBe((byte)50);
+ c.A.ShouldBe((byte)50);
+ }
+
+ [Fact]
+ public void MultiplyOperator_should_clamp_above_255()
+ {
+ var c = new Color((byte)200, (byte)200, (byte)200, (byte)200) * 3f;
+ c.R.ShouldBe((byte)255);
+ }
+
+ [Fact]
+ public void MultiplyOperator_should_clamp_negative_to_zero()
+ {
+ var c = new Color((byte)200, (byte)200, (byte)200, (byte)200) * -1f;
+ c.R.ShouldBe((byte)0);
+ }
+
+ [Fact]
+ public void Explicit_Vector3_conversion_should_normalize_to_0_to_1_range()
+ {
+ var v = (Vector3)new Color((byte)255, (byte)128, (byte)0);
+ v.X.ShouldBe(1f, 0.001f);
+ v.Y.ShouldBe(128f / 255f, 0.001f);
+ v.Z.ShouldBe(0f, 0.001f);
+ }
+
+ [Fact]
+ public void Equals_object_should_return_true_when_color_is_equivalent()
+ {
+ var a = new Color((byte)1, (byte)2, (byte)3, (byte)4);
+ object b = new Color((byte)1, (byte)2, (byte)3, (byte)4);
+ a.Equals(b).ShouldBeTrue();
+ }
+
+ [Fact]
+ public void Equals_object_should_return_false_for_non_Color()
+ {
+ var a = new Color((byte)1, (byte)2, (byte)3, (byte)4);
+ a.Equals("not a color").ShouldBeFalse();
+ }
+
+ [Fact]
+ public void Equals_object_should_return_false_for_null()
+ {
+ var a = new Color((byte)1, (byte)2, (byte)3, (byte)4);
+ a.Equals(null).ShouldBeFalse();
+ }
+
+ [Fact]
+ public void GetHashCode_should_be_equal_for_equal_colors()
+ {
+ var a = new Color((byte)1, (byte)2, (byte)3, (byte)4);
+ var b = new Color((byte)1, (byte)2, (byte)3, (byte)4);
+ a.GetHashCode().ShouldBe(b.GetHashCode());
+ }
+
+ [Fact]
+ public void GetHashCode_should_differ_for_different_colors()
+ {
+ var a = new Color((byte)1, (byte)2, (byte)3, (byte)4);
+ var b = new Color((byte)1, (byte)2, (byte)3, (byte)9);
+ a.GetHashCode().ShouldNotBe(b.GetHashCode());
+ }
+
+ [Fact]
+ public void White_should_have_max_components()
+ {
+ Color.White.R.ShouldBe((byte)0xFF);
+ Color.White.G.ShouldBe((byte)0xFF);
+ Color.White.B.ShouldBe((byte)0xFF);
+ Color.White.A.ShouldBe((byte)0xFF);
+ }
+
+ [Fact]
+ public void Black_should_have_zero_rgb_and_full_alpha()
+ {
+ Color.Black.R.ShouldBe((byte)0);
+ Color.Black.G.ShouldBe((byte)0);
+ Color.Black.B.ShouldBe((byte)0);
+ Color.Black.A.ShouldBe((byte)0xFF);
+ }
+
+ [Fact]
+ public void Red_should_be_pure_red()
+ {
+ Color.Red.R.ShouldBe((byte)0xFF);
+ Color.Red.G.ShouldBe((byte)0);
+ Color.Red.B.ShouldBe((byte)0);
+ }
+
+ [Fact]
+ public void Green_should_match_HTML_green_0x008000()
+ {
+ // HTML/CSS "Green" is 0x008000, not 0x00FF00
+ Color.Green.R.ShouldBe((byte)0);
+ Color.Green.G.ShouldBe((byte)0x80);
+ Color.Green.B.ShouldBe((byte)0);
+ }
+
+ [Fact]
+ public void Blue_should_be_pure_blue()
+ {
+ Color.Blue.R.ShouldBe((byte)0);
+ Color.Blue.G.ShouldBe((byte)0);
+ Color.Blue.B.ShouldBe((byte)0xFF);
+ }
+
+ [Fact]
+ public void Transparent_should_have_zero_alpha()
+ {
+ Color.Transparent.A.ShouldBe((byte)0);
+ }
+}
diff --git a/test/SampSharp.OpenMp.Entities.Tests/EnvironmentsTests.cs b/test/SampSharp.OpenMp.Entities.Tests/EnvironmentsTests.cs
new file mode 100644
index 00000000..59d08ed7
--- /dev/null
+++ b/test/SampSharp.OpenMp.Entities.Tests/EnvironmentsTests.cs
@@ -0,0 +1,78 @@
+using System.Reflection;
+using Moq;
+using SampSharp.Entities;
+using SampSharp.OpenMp.Core.Api;
+using Shouldly;
+using Xunit;
+
+namespace SampSharp.OpenMp.Entities.Tests;
+
+public class EnvironmentsTests
+{
+ [Fact]
+ public void Production_should_equal_string_Production()
+ {
+ Environments.Production.ShouldBe("Production");
+ }
+
+ [Fact]
+ public void Development_should_equal_string_Development()
+ {
+ Environments.Development.ShouldBe("Development");
+ }
+
+ [Fact]
+ public void Staging_should_equal_string_Staging()
+ {
+ Environments.Staging.ShouldBe("Staging");
+ }
+}
+
+public class SampSharpEnvironmentTests
+{
+ [Fact]
+ public void Ctor_should_store_constructor_arguments()
+ {
+ var asm = typeof(SampSharpEnvironmentTests).Assembly;
+ var core = default(ICore);
+ var components = default(IComponentList);
+ var handles = new Mock().Object;
+ var env = new SampSharpEnvironment(asm, core, components, handles, Environments.Development);
+ env.EntryAssembly.ShouldBe(asm);
+ env.Core.ShouldBe(core);
+ env.Components.ShouldBe(components);
+ env.SafeComponentHandleProvider.ShouldBe(handles);
+ env.EnvironmentName.ShouldBe(Environments.Development);
+ }
+
+ [Fact]
+ public void Equals_should_be_true_for_records_with_identical_values()
+ {
+ var asm = Assembly.GetExecutingAssembly();
+ var handles = new Mock().Object;
+ var a = new SampSharpEnvironment(asm, default, default, handles, "X");
+ var b = new SampSharpEnvironment(asm, default, default, handles, "X");
+ a.ShouldBe(b);
+ }
+
+ [Fact]
+ public void Equals_should_be_false_for_records_with_different_environment_name()
+ {
+ var asm = Assembly.GetExecutingAssembly();
+ var handles = new Mock().Object;
+ var a = new SampSharpEnvironment(asm, default, default, handles, "Production");
+ var b = new SampSharpEnvironment(asm, default, default, handles, "Development");
+ a.ShouldNotBe(b);
+ }
+
+ [Fact]
+ public void With_expression_should_replace_field()
+ {
+ var asm = Assembly.GetExecutingAssembly();
+ var handles = new Mock().Object;
+ var original = new SampSharpEnvironment(asm, default, default, handles, "Production");
+ var modified = original with { EnvironmentName = "Staging" };
+ modified.EnvironmentName.ShouldBe("Staging");
+ original.EnvironmentName.ShouldBe("Production");
+ }
+}
diff --git a/test/SampSharp.OpenMp.Entities.Tests/OmpLoggerOptionsTests.cs b/test/SampSharp.OpenMp.Entities.Tests/OmpLoggerOptionsTests.cs
new file mode 100644
index 00000000..7afd8e87
--- /dev/null
+++ b/test/SampSharp.OpenMp.Entities.Tests/OmpLoggerOptionsTests.cs
@@ -0,0 +1,37 @@
+using SampSharp.Entities;
+using Shouldly;
+using Xunit;
+using OmpLogLevel = SampSharp.OpenMp.Core.Api.LogLevel;
+
+namespace SampSharp.OpenMp.Entities.Tests;
+
+public class OmpLoggerOptionsTests
+{
+ [Fact]
+ public void Defaults_should_match_documented_level_mapping()
+ {
+ var options = new OmpLoggerOptions();
+ options.TraceLevel.ShouldBe(OmpLogLevel.Message);
+ options.DebugLevel.ShouldBe(OmpLogLevel.Message);
+ options.InformationLevel.ShouldBe(OmpLogLevel.Message);
+ options.WarningLevel.ShouldBe(OmpLogLevel.Warning);
+ options.ErrorLevel.ShouldBe(OmpLogLevel.Error);
+ options.CriticalLevel.ShouldBe(OmpLogLevel.Error);
+ }
+
+ [Fact]
+ public void Properties_should_be_settable()
+ {
+ var options = new OmpLoggerOptions
+ {
+ TraceLevel = OmpLogLevel.Debug,
+ DebugLevel = OmpLogLevel.Debug,
+ InformationLevel = OmpLogLevel.Message,
+ WarningLevel = OmpLogLevel.Warning,
+ ErrorLevel = OmpLogLevel.Error,
+ CriticalLevel = OmpLogLevel.Error
+ };
+ options.TraceLevel.ShouldBe(OmpLogLevel.Debug);
+ options.DebugLevel.ShouldBe(OmpLogLevel.Debug);
+ }
+}
diff --git a/test/SampSharp.OpenMp.Entities.Tests/OmpLoggerTests.cs b/test/SampSharp.OpenMp.Entities.Tests/OmpLoggerTests.cs
new file mode 100644
index 00000000..4c083a29
--- /dev/null
+++ b/test/SampSharp.OpenMp.Entities.Tests/OmpLoggerTests.cs
@@ -0,0 +1,61 @@
+using System.Text;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.ObjectPool;
+using SampSharp.Entities;
+using Shouldly;
+using Xunit;
+using OmpLogLevel = SampSharp.OpenMp.Core.Api.LogLevel;
+
+namespace SampSharp.OpenMp.Entities.Tests;
+
+public class OmpLoggerTests
+{
+ private static OmpLogger CreateLogger(OmpLoggerOptions? options = null)
+ {
+ var pool = new DefaultObjectPool(new StringBuilderPooledObjectPolicy());
+ return new OmpLogger(default, options ?? new OmpLoggerOptions(), "test", pool);
+ }
+
+ [Fact]
+ public void IsEnabled_should_return_false_for_None()
+ {
+ CreateLogger().IsEnabled(LogLevel.None).ShouldBeFalse();
+ }
+
+ [Theory]
+ [InlineData(LogLevel.Trace)]
+ [InlineData(LogLevel.Debug)]
+ [InlineData(LogLevel.Information)]
+ [InlineData(LogLevel.Warning)]
+ [InlineData(LogLevel.Error)]
+ [InlineData(LogLevel.Critical)]
+ public void IsEnabled_should_return_true_for_all_non_None_levels(LogLevel level)
+ {
+ CreateLogger().IsEnabled(level).ShouldBeTrue();
+ }
+
+ [Fact]
+ public void BeginScope_should_return_null()
+ {
+ CreateLogger().BeginScope(new { test = 1 }).ShouldBeNull();
+ }
+
+ [Fact]
+ public void Options_should_be_settable()
+ {
+ var logger = CreateLogger();
+ var newOptions = new OmpLoggerOptions { TraceLevel = OmpLogLevel.Debug };
+ logger.Options = newOptions;
+ logger.Options.ShouldBe(newOptions);
+ }
+
+ [Fact]
+ public void Log_should_short_circuit_on_None_level()
+ {
+ // Verifies Log doesn't reach the native writer call when level is None.
+ // If it did, the default ILogger struct (zeroed function pointers) would crash.
+ var logger = CreateLogger();
+ Should.NotThrow(() => logger.Log(LogLevel.None, new EventId(0), state: "msg", exception: null,
+ formatter: (_, _) => "msg"));
+ }
+}
diff --git a/test/SampSharp.OpenMp.Entities.Tests/PlayerSpawnDataTests.cs b/test/SampSharp.OpenMp.Entities.Tests/PlayerSpawnDataTests.cs
new file mode 100644
index 00000000..0f6d103c
--- /dev/null
+++ b/test/SampSharp.OpenMp.Entities.Tests/PlayerSpawnDataTests.cs
@@ -0,0 +1,102 @@
+using System.Numerics;
+using SampSharp.Entities.SAMP;
+using SampSharp.OpenMp.Core.Api;
+using Shouldly;
+using Xunit;
+
+namespace SampSharp.OpenMp.Entities.Tests;
+
+public class PlayerSpawnDataTests
+{
+ [Fact]
+ public void Default_ctor_should_initialize_Weapons()
+ {
+ var data = new PlayerSpawnData();
+ data.Weapons.ShouldNotBeNull();
+ }
+
+ [Fact]
+ public void Default_ctor_should_set_default_values_for_Team_Skin_Angle_Location()
+ {
+ var data = new PlayerSpawnData();
+ data.Team.ShouldBe(0);
+ data.Skin.ShouldBe(0);
+ data.Angle.ShouldBe(0f);
+ data.Location.ShouldBe(Vector3.Zero);
+ }
+
+ [Fact]
+ public void Parameterized_ctor_should_set_all_fields()
+ {
+ var weapons = new PlayerWeaponSlots();
+ weapons.Add(new PlayerWeaponSlot(Weapon.Colt45, 50));
+ var data = new PlayerSpawnData(team: 3, skin: 7, location: new Vector3(1, 2, 3), angle: 90f, weapons: weapons);
+ data.Team.ShouldBe(3);
+ data.Skin.ShouldBe(7);
+ data.Location.ShouldBe(new Vector3(1, 2, 3));
+ data.Angle.ShouldBe(90f);
+ data.Weapons.ShouldBe(weapons);
+ }
+
+ [Fact]
+ public void Properties_should_be_settable()
+ {
+ var data = new PlayerSpawnData
+ {
+ Team = 5,
+ Skin = 99,
+ Location = new Vector3(10, 20, 30),
+ Angle = 45f
+ };
+ data.Team.ShouldBe(5);
+ data.Skin.ShouldBe(99);
+ data.Location.ShouldBe(new Vector3(10, 20, 30));
+ data.Angle.ShouldBe(45f);
+ }
+
+ [Fact]
+ public void ToOmpData_should_serialize_fields_to_PlayerClass()
+ {
+ var weapons = new PlayerWeaponSlots();
+ weapons.Add(new PlayerWeaponSlot(Weapon.Colt45, 50));
+ var data = new PlayerSpawnData(team: 3, skin: 7, location: new Vector3(1, 2, 3), angle: 90f, weapons: weapons);
+ var omp = data.ToOmpData();
+ omp.Team.ShouldBe(3);
+ omp.Skin.ShouldBe(7);
+ omp.Spawn.ShouldBe(new Vector3(1, 2, 3));
+ omp.Angle.ShouldBe(90f);
+ omp.Weapons.Data[2].Id.ShouldBe((byte)Weapon.Colt45);
+ omp.Weapons.Data[2].Ammo.ShouldBe(50);
+ }
+
+ [Fact]
+ public void FromOmpData_should_deserialize_fields_from_PlayerClass()
+ {
+ var weaponSlotData = new WeaponSlotData[WeaponSlots.MAX_WEAPON_SLOTS];
+ weaponSlotData[2] = new WeaponSlotData((byte)Weapon.Colt45, 75);
+ var ompClass = new PlayerClass(team: 2, skin: 8, spawn: new Vector3(4, 5, 6), angle: 180f, weapons: new WeaponSlots(weaponSlotData));
+ var data = PlayerSpawnData.FromOmpData(ref ompClass);
+ data.Team.ShouldBe(2);
+ data.Skin.ShouldBe(8);
+ data.Location.ShouldBe(new Vector3(4, 5, 6));
+ data.Angle.ShouldBe(180f);
+ data.Weapons[2].Weapon.ShouldBe(Weapon.Colt45);
+ data.Weapons[2].Ammo.ShouldBe(75);
+ }
+
+ [Fact]
+ public void ToOmpData_then_FromOmpData_should_preserve_fields()
+ {
+ var weapons = new PlayerWeaponSlots();
+ weapons.Add(new PlayerWeaponSlot(Weapon.Grenade, 3));
+ var original = new PlayerSpawnData(1, 50, new Vector3(7, 8, 9), 270f, weapons);
+ var omp = original.ToOmpData();
+ var roundtrip = PlayerSpawnData.FromOmpData(ref omp);
+ roundtrip.Team.ShouldBe(original.Team);
+ roundtrip.Skin.ShouldBe(original.Skin);
+ roundtrip.Location.ShouldBe(original.Location);
+ roundtrip.Angle.ShouldBe(original.Angle);
+ roundtrip.Weapons[8].Weapon.ShouldBe(Weapon.Grenade);
+ roundtrip.Weapons[8].Ammo.ShouldBe(3);
+ }
+}
diff --git a/test/SampSharp.OpenMp.Entities.Tests/PlayerWeaponSlotsTests.cs b/test/SampSharp.OpenMp.Entities.Tests/PlayerWeaponSlotsTests.cs
new file mode 100644
index 00000000..82e29450
--- /dev/null
+++ b/test/SampSharp.OpenMp.Entities.Tests/PlayerWeaponSlotsTests.cs
@@ -0,0 +1,180 @@
+using System.Linq;
+using SampSharp.Entities.SAMP;
+using SampSharp.OpenMp.Core.Api;
+using Shouldly;
+using Xunit;
+
+namespace SampSharp.OpenMp.Entities.Tests;
+
+public class PlayerWeaponSlotsTests
+{
+ [Fact]
+ public void Default_ctor_should_create_empty_slots()
+ {
+ var slots = new PlayerWeaponSlots();
+ slots.Count().ShouldBe(0);
+ }
+
+ [Fact]
+ public void Indexer_should_return_default_slots_after_default_ctor()
+ {
+ var slots = new PlayerWeaponSlots();
+ for (var i = 0; i < WeaponSlots.MAX_WEAPON_SLOTS; i++)
+ {
+ slots[i].Weapon.ShouldBe(Weapon.None);
+ slots[i].Ammo.ShouldBe(0);
+ }
+ }
+
+ [Fact]
+ public void Ctor_should_succeed_when_data_length_matches_max_slots()
+ {
+ var data = new WeaponSlotData[WeaponSlots.MAX_WEAPON_SLOTS];
+ Should.NotThrow(() => new PlayerWeaponSlots(data));
+ }
+
+ [Fact]
+ public void Ctor_should_throw_when_data_length_does_not_match_max_slots()
+ {
+ var data = new WeaponSlotData[WeaponSlots.MAX_WEAPON_SLOTS - 1];
+ Should.Throw(() => new PlayerWeaponSlots(data));
+ }
+
+ [Fact]
+ public void Ctor_should_throw_when_data_is_null()
+ {
+ Should.Throw(() => new PlayerWeaponSlots(null!));
+ }
+
+ [Fact]
+ public void Add_should_place_weapon_in_correct_slot()
+ {
+ // Colt45 (id 22) -> slot 2
+ var slots = new PlayerWeaponSlots();
+ slots.Add(new PlayerWeaponSlot(Weapon.Colt45, 100));
+ slots[2].Weapon.ShouldBe(Weapon.Colt45);
+ slots[2].Ammo.ShouldBe(100);
+ }
+
+ [Fact]
+ public void Add_should_throw_for_weapon_with_no_valid_slot()
+ {
+ var slots = new PlayerWeaponSlots();
+ // Connect (id 200) is beyond the WeaponInfo table -> slot -1
+ Should.Throw(() => slots.Add(new PlayerWeaponSlot(Weapon.Connect, 1)));
+ }
+
+ [Fact]
+ public void Add_should_replace_existing_weapon_in_same_slot()
+ {
+ // Both Colt45 (22) and Silenced (23) share slot 2
+ var slots = new PlayerWeaponSlots();
+ slots.Add(new PlayerWeaponSlot(Weapon.Colt45, 50));
+ slots.Add(new PlayerWeaponSlot(Weapon.Silenced, 75));
+ slots[2].Weapon.ShouldBe(Weapon.Silenced);
+ slots[2].Ammo.ShouldBe(75);
+ }
+
+ [Fact]
+ public void Reset_should_clear_specified_slot()
+ {
+ var slots = new PlayerWeaponSlots();
+ slots.Add(new PlayerWeaponSlot(Weapon.Colt45, 100));
+ slots.Reset((WeaponSlot)2);
+ slots[2].Weapon.ShouldBe(Weapon.None);
+ slots[2].Ammo.ShouldBe(0);
+ }
+
+ [Fact]
+ public void Reset_should_throw_when_slot_is_negative()
+ {
+ var slots = new PlayerWeaponSlots();
+ Should.Throw(() => slots.Reset((WeaponSlot)(-1)));
+ }
+
+ [Fact]
+ public void Reset_should_throw_when_slot_is_at_max()
+ {
+ var slots = new PlayerWeaponSlots();
+ Should.Throw(() => slots.Reset((WeaponSlot)WeaponSlots.MAX_WEAPON_SLOTS));
+ }
+
+ [Fact]
+ public void Remove_by_weapon_should_return_true_when_present()
+ {
+ var slots = new PlayerWeaponSlots();
+ slots.Add(new PlayerWeaponSlot(Weapon.Colt45, 100));
+ slots.Remove(Weapon.Colt45).ShouldBeTrue();
+ slots[2].Ammo.ShouldBe(0);
+ }
+
+ [Fact]
+ public void Remove_by_weapon_should_return_false_when_slot_empty()
+ {
+ var slots = new PlayerWeaponSlots();
+ slots.Remove(Weapon.Colt45).ShouldBeFalse();
+ }
+
+ [Fact]
+ public void Remove_by_item_should_delegate_to_remove_by_weapon()
+ {
+ var slots = new PlayerWeaponSlots();
+ slots.Add(new PlayerWeaponSlot(Weapon.Colt45, 50));
+ slots.Remove(new PlayerWeaponSlot(Weapon.Colt45, 9999)).ShouldBeTrue();
+ slots.Remove(new PlayerWeaponSlot(Weapon.Colt45, 0)).ShouldBeFalse();
+ }
+
+ [Fact]
+ public void Remove_should_throw_for_weapon_with_no_valid_slot()
+ {
+ var slots = new PlayerWeaponSlots();
+ Should.Throw(() => slots.Remove(Weapon.Connect));
+ }
+
+ [Fact]
+ public void Indexer_should_throw_for_negative_index()
+ {
+ var slots = new PlayerWeaponSlots();
+ Should.Throw(() => slots[-1]);
+ }
+
+ [Fact]
+ public void Indexer_should_throw_at_max_index()
+ {
+ var slots = new PlayerWeaponSlots();
+ Should.Throw(() => slots[WeaponSlots.MAX_WEAPON_SLOTS]);
+ }
+
+ [Fact]
+ public void GetEnumerator_should_skip_empty_slots()
+ {
+ var slots = new PlayerWeaponSlots();
+ slots.Add(new PlayerWeaponSlot(Weapon.Colt45, 100));
+ slots.Add(new PlayerWeaponSlot(Weapon.Grenade, 5));
+ var list = slots.ToList();
+ list.Count.ShouldBe(2);
+ list.ShouldContain(s => s.Weapon == Weapon.Colt45);
+ list.ShouldContain(s => s.Weapon == Weapon.Grenade);
+ }
+
+ [Fact]
+ public void GetEnumerator_should_work_via_non_generic_IEnumerable()
+ {
+ var slots = new PlayerWeaponSlots();
+ slots.Add(new PlayerWeaponSlot(Weapon.Colt45, 100));
+ var enumerator = ((System.Collections.IEnumerable)slots).GetEnumerator();
+ enumerator.MoveNext().ShouldBeTrue();
+ ((PlayerWeaponSlot)enumerator.Current!).Weapon.ShouldBe(Weapon.Colt45);
+ }
+
+ [Fact]
+ public void ToOmpData_should_wrap_internal_array()
+ {
+ var slots = new PlayerWeaponSlots();
+ slots.Add(new PlayerWeaponSlot(Weapon.Colt45, 100));
+ var omp = slots.ToOmpData();
+ omp.Data.Length.ShouldBe(WeaponSlots.MAX_WEAPON_SLOTS);
+ omp.Data[2].Id.ShouldBe((byte)Weapon.Colt45);
+ omp.Data[2].Ammo.ShouldBe(100);
+ }
+}
diff --git a/test/SampSharp.OpenMp.Entities.Tests/TimerAttributeTests.cs b/test/SampSharp.OpenMp.Entities.Tests/TimerAttributeTests.cs
new file mode 100644
index 00000000..e0736c6a
--- /dev/null
+++ b/test/SampSharp.OpenMp.Entities.Tests/TimerAttributeTests.cs
@@ -0,0 +1,23 @@
+using SampSharp.Entities;
+using Shouldly;
+using Xunit;
+
+namespace SampSharp.OpenMp.Entities.Tests;
+
+public class TimerAttributeTests
+{
+ [Fact]
+ public void Ctor_should_set_Interval_property()
+ {
+ var attr = new TimerAttribute(123.5);
+ attr.Interval.ShouldBe(123.5);
+ }
+
+ [Fact]
+ public void Interval_should_be_settable()
+ {
+ var attr = new TimerAttribute(10);
+ attr.Interval = 500;
+ attr.Interval.ShouldBe(500);
+ }
+}
diff --git a/test/SampSharp.OpenMp.Entities.Tests/TimerServiceExtensionsTests.cs b/test/SampSharp.OpenMp.Entities.Tests/TimerServiceExtensionsTests.cs
new file mode 100644
index 00000000..b9ba9f2e
--- /dev/null
+++ b/test/SampSharp.OpenMp.Entities.Tests/TimerServiceExtensionsTests.cs
@@ -0,0 +1,79 @@
+using System.Reflection;
+using Moq;
+using SampSharp.Entities;
+using Shouldly;
+using Xunit;
+
+namespace SampSharp.OpenMp.Entities.Tests;
+
+public class TimerServiceExtensionsTests
+{
+ private sealed class TestTarget
+ {
+ public int CallCount;
+ public void Tick() => CallCount++;
+ }
+
+ private static MethodInfo TickMethod => typeof(TestTarget).GetMethod(nameof(TestTarget.Tick))!;
+
+ [Fact]
+ public void Start_should_throw_when_timerService_is_null()
+ {
+ Should.Throw(() =>
+ TimerServiceExtensions.Start(null!, new TestTarget(), TickMethod, TimeSpan.FromSeconds(1)));
+ }
+
+ [Fact]
+ public void Start_should_throw_when_target_is_null()
+ {
+ var mock = new Mock();
+ Should.Throw(() =>
+ mock.Object.Start(null!, TickMethod, TimeSpan.FromSeconds(1)));
+ }
+
+ [Fact]
+ public void Start_should_throw_when_method_is_null()
+ {
+ var mock = new Mock();
+ Should.Throw(() =>
+ mock.Object.Start(new TestTarget(), null!, TimeSpan.FromSeconds(1)));
+ }
+
+ [Fact]
+ public void Start_should_throw_when_interval_is_zero()
+ {
+ var mock = new Mock();
+ Should.Throw(() =>
+ mock.Object.Start(new TestTarget(), TickMethod, TimeSpan.Zero));
+ }
+
+ [Fact]
+ public void Start_should_throw_when_method_is_not_member_of_target()
+ {
+ var mock = new Mock();
+ // Foreign target: TestTarget.Tick is not a member of object.
+ Should.Throw(() =>
+ mock.Object.Start(new object(), TickMethod, TimeSpan.FromMilliseconds(50)));
+ }
+
+ [Fact]
+ public void Start_should_invoke_underlying_timerService_Start_and_capture_target_method()
+ {
+ var mock = new Mock();
+ Action? capturedAction = null;
+ var interval = TimeSpan.FromMilliseconds(100);
+ mock.Setup(s => s.Start(It.IsAny>(), interval))
+ .Callback, TimeSpan>((a, _) => capturedAction = a)
+ .Returns((TimerReference)null!);
+
+ var target = new TestTarget();
+ mock.Object.Start(target, TickMethod, interval);
+
+ mock.Verify(s => s.Start(It.IsAny>(), interval), Times.Once);
+ capturedAction.ShouldNotBeNull();
+
+ // Invoke the captured action and verify it triggers Tick() on target.
+ capturedAction!(new Mock().Object);
+ target.CallCount.ShouldBe(1);
+ }
+}
diff --git a/test/SampSharp.OpenMp.Entities.Tests/VehicleParametersTests.cs b/test/SampSharp.OpenMp.Entities.Tests/VehicleParametersTests.cs
new file mode 100644
index 00000000..d6ac05f2
--- /dev/null
+++ b/test/SampSharp.OpenMp.Entities.Tests/VehicleParametersTests.cs
@@ -0,0 +1,93 @@
+using System.Runtime.CompilerServices;
+using SampSharp.Entities.SAMP;
+using SampSharp.OpenMp.Core.Api;
+using Shouldly;
+using Xunit;
+
+namespace SampSharp.OpenMp.Entities.Tests;
+
+public class VehicleParametersTests
+{
+ [Fact]
+ public void Default_struct_should_have_Off_values_via_zero_init()
+ {
+ var p = default(VehicleParameters);
+ p.Engine.ShouldBe(VehicleParameterValue.Off);
+ }
+
+ [Fact]
+ public void With_expression_should_modify_only_specified_fields()
+ {
+ var original = new VehicleParameters(
+ VehicleParameterValue.Off, VehicleParameterValue.Off, VehicleParameterValue.Off, VehicleParameterValue.Off,
+ VehicleParameterValue.Off, VehicleParameterValue.Off, VehicleParameterValue.Off, VehicleParameterValue.Off,
+ VehicleParameterValue.Off, VehicleParameterValue.Off, VehicleParameterValue.Off, VehicleParameterValue.Off,
+ VehicleParameterValue.Off, VehicleParameterValue.Off, VehicleParameterValue.Off, VehicleParameterValue.Off);
+ var modified = original with { Engine = VehicleParameterValue.On, Siren = VehicleParameterValue.On };
+ modified.Engine.ShouldBe(VehicleParameterValue.On);
+ modified.Siren.ShouldBe(VehicleParameterValue.On);
+ modified.Lights.ShouldBe(VehicleParameterValue.Off);
+ original.Engine.ShouldBe(VehicleParameterValue.Off);
+ }
+
+ [Fact]
+ public void Equals_should_be_true_for_record_struct_with_identical_values()
+ {
+ var a = new VehicleParameters(
+ VehicleParameterValue.On, VehicleParameterValue.Off, VehicleParameterValue.Unset, VehicleParameterValue.Off,
+ VehicleParameterValue.Off, VehicleParameterValue.Off, VehicleParameterValue.Off, VehicleParameterValue.Off,
+ VehicleParameterValue.Off, VehicleParameterValue.Off, VehicleParameterValue.Off, VehicleParameterValue.Off,
+ VehicleParameterValue.Off, VehicleParameterValue.Off, VehicleParameterValue.Off, VehicleParameterValue.Off);
+ var b = a with { };
+ a.ShouldBe(b);
+ }
+
+ [Fact]
+ public void Layout_should_match_VehicleParams_size()
+ {
+ Unsafe.SizeOf().ShouldBe(Unsafe.SizeOf());
+ }
+
+ [Fact]
+ public void Roundtrip_through_VehicleParams_should_preserve_all_fields()
+ {
+ var native = new VehicleParams(
+ engine: 1, lights: 0, alarm: -1, doors: 1,
+ bonnet: 0, boot: 1, objective: 0, siren: 1,
+ doorDriver: 0, doorPassenger: 1, doorBackLeft: 0, doorBackRight: 1,
+ windowDriver: 0, windowPassenger: 1, windowBackLeft: 0, windowBackRight: 1);
+
+ var managed = ReinterpretFromParams(ref native);
+
+ managed.Engine.ShouldBe(VehicleParameterValue.On);
+ managed.Lights.ShouldBe(VehicleParameterValue.Off);
+ managed.Alarm.ShouldBe(VehicleParameterValue.Unset);
+ managed.Doors.ShouldBe(VehicleParameterValue.On);
+ managed.Bonnet.ShouldBe(VehicleParameterValue.Off);
+ managed.Boot.ShouldBe(VehicleParameterValue.On);
+ managed.Objective.ShouldBe(VehicleParameterValue.Off);
+ managed.Siren.ShouldBe(VehicleParameterValue.On);
+ managed.DoorDriver.ShouldBe(VehicleParameterValue.Off);
+ managed.DoorPassenger.ShouldBe(VehicleParameterValue.On);
+ managed.DoorBackLeft.ShouldBe(VehicleParameterValue.Off);
+ managed.DoorBackRight.ShouldBe(VehicleParameterValue.On);
+ managed.WindowDriver.ShouldBe(VehicleParameterValue.Off);
+ managed.WindowPassenger.ShouldBe(VehicleParameterValue.On);
+ managed.WindowBackLeft.ShouldBe(VehicleParameterValue.Off);
+ managed.WindowBackRight.ShouldBe(VehicleParameterValue.On);
+
+ var native2 = ReinterpretToParams(ref managed);
+ native2.ShouldBe(native);
+ }
+
+ // FromParams/ToParams are internal in VehicleParameters; exercise the same Unsafe.As reinterpret here.
+ private static VehicleParameters ReinterpretFromParams(ref VehicleParams value)
+ {
+ return Unsafe.As(ref value);
+ }
+
+ private static VehicleParams ReinterpretToParams(ref VehicleParameters value)
+ {
+ return Unsafe.As(ref value);
+ }
+}
diff --git a/test/TestMode.Entities.ApiTests/ConfigTests.cs b/test/TestMode.Entities.ApiTests/ConfigTests.cs
index 71027738..0525e01c 100644
--- a/test/TestMode.Entities.ApiTests/ConfigTests.cs
+++ b/test/TestMode.Entities.ApiTests/ConfigTests.cs
@@ -1,5 +1,6 @@
using Microsoft.Extensions.DependencyInjection;
using SampSharp.Entities;
+using SampSharp.Entities.SAMP;
using SampSharp.OpenMp.Core.Api;
using Shouldly;
using Xunit;
@@ -9,10 +10,12 @@ namespace TestMode.Entities.ApiTests;
public class ConfigTests : TestBase
{
private IConfig _config;
+ private IConfigService _configService;
public ConfigTests()
{
_config = Services.GetRequiredService().Core.GetConfig();
+ _configService = Services.GetRequiredService();
}
[Fact]
@@ -23,4 +26,87 @@ public void GetOptions_should_succeed()
options.Count.ShouldBeGreaterThan(10);
options["sampsharp.directory"].ShouldBe(ConfigOptionType.String);
}
+
+ [Fact]
+ public void ConfigService_GetOptions_should_return_non_empty_dictionary()
+ {
+ var options = _configService.GetOptions();
+ options.ShouldNotBeEmpty();
+ options.ContainsKey("sampsharp.directory").ShouldBeTrue();
+ }
+
+ [Fact]
+ public void ConfigService_GetString_should_return_value_for_known_key()
+ {
+ var value = _configService.GetString("sampsharp.directory");
+ value.ShouldNotBeNull();
+ }
+
+ [Fact]
+ public void ConfigService_GetString_should_return_null_for_missing_key()
+ {
+ var value = _configService.GetString("this_key_does_not_exist");
+ value.ShouldBeNull();
+ }
+
+ [Fact]
+ public void ConfigService_GetInt_should_return_null_for_missing_key()
+ {
+ var value = _configService.GetInt("this_key_does_not_exist");
+ value.ShouldBeNull();
+ }
+
+ [Fact]
+ public void ConfigService_GetFloat_should_return_null_for_missing_key()
+ {
+ var value = _configService.GetFloat("this_key_does_not_exist");
+ value.ShouldBeNull();
+ }
+
+ [Fact]
+ public void ConfigService_GetBool_should_return_null_for_missing_key()
+ {
+ var value = _configService.GetBool("this_key_does_not_exist");
+ value.ShouldBeNull();
+ }
+
+ [Fact]
+ public void ConfigService_GetStrings_should_return_empty_for_missing_key()
+ {
+ var values = _configService.GetStrings("this_key_does_not_exist");
+ values.ShouldBeEmpty();
+ }
+
+ [Fact]
+ public void ConfigService_GetValueType_should_return_string_for_known_key()
+ {
+ var type = _configService.GetValueType("sampsharp.directory");
+ type.ShouldBe(ConfigOptionType.String);
+ }
+
+ [Fact]
+ public void ConfigService_GetInt_should_return_value_for_int_key()
+ {
+ var options = _configService.GetOptions();
+ var intKey = options.FirstOrDefault(kv => kv.Value == ConfigOptionType.Int).Key;
+
+ if (intKey == null)
+ return; // no int keys in this config — skip
+
+ var value = _configService.GetInt(intKey);
+ value.ShouldNotBeNull();
+ }
+
+ [Fact]
+ public void ConfigService_GetBool_should_return_value_for_bool_key()
+ {
+ var options = _configService.GetOptions();
+ var boolKey = options.FirstOrDefault(kv => kv.Value == ConfigOptionType.Bool).Key;
+
+ if (boolKey == null)
+ return; // no bool keys in this config — skip
+
+ var value = _configService.GetBool(boolKey);
+ value.ShouldNotBeNull();
+ }
}
\ No newline at end of file
diff --git a/test/TestMode.Entities.ApiTests/DialogServiceTests.cs b/test/TestMode.Entities.ApiTests/DialogServiceTests.cs
new file mode 100644
index 00000000..04cc52e1
--- /dev/null
+++ b/test/TestMode.Entities.ApiTests/DialogServiceTests.cs
@@ -0,0 +1,72 @@
+using Microsoft.Extensions.DependencyInjection;
+using SampSharp.Entities.SAMP;
+using Shouldly;
+using Xunit;
+
+namespace TestMode.Entities.ApiTests;
+
+public class DialogServiceTests : TestBase
+{
+ private IDialogService Sut => Services.GetRequiredService();
+
+ [Fact]
+ public void Show_MessageDialog_should_succeed()
+ {
+ var dialog = new MessageDialog("Caption", "Content", "OK", "Cancel");
+ Should.NotThrow(() => Sut.Show(Player, dialog, _ => { }));
+ }
+
+ [Fact]
+ public void Show_MessageDialog_with_single_button_should_succeed()
+ {
+ var dialog = new MessageDialog("Caption", "Content", "OK");
+ Should.NotThrow(() => Sut.Show(Player, dialog, _ => { }));
+ }
+
+ [Fact]
+ public void Show_InputDialog_should_succeed()
+ {
+ var dialog = new InputDialog("Caption", "Enter something:", "OK", "Cancel");
+ Should.NotThrow(() => Sut.Show(Player, dialog, _ => { }));
+ }
+
+ [Fact]
+ public void Show_InputDialog_password_should_succeed()
+ {
+ var dialog = new InputDialog("Caption", "Enter password:", "OK") { IsPassword = true };
+ Should.NotThrow(() => Sut.Show(Player, dialog, _ => { }));
+ }
+
+ [Fact]
+ public void Show_ListDialog_should_succeed()
+ {
+ var dialog = new ListDialog("Caption", "Select", "Cancel");
+ dialog.Add("Row 1");
+ dialog.Add("Row 2");
+ Should.NotThrow(() => Sut.Show(Player, dialog, _ => { }));
+ }
+
+ [Fact]
+ public void Show_ListDialog_with_tag_should_succeed()
+ {
+ var dialog = new ListDialog("Caption", "Select");
+ dialog.Add("Row 1", tag: 42);
+ Should.NotThrow(() => Sut.Show(Player, dialog, _ => { }));
+ }
+
+ [Fact]
+ public void Show_TablistDialog_without_headers_should_succeed()
+ {
+ var dialog = new TablistDialog("Caption", "Select", "Cancel", columnCount: 2);
+ dialog.Add("Col1", "Col2");
+ Should.NotThrow(() => Sut.Show(Player, dialog, _ => { }));
+ }
+
+ [Fact]
+ public void Show_TablistDialog_with_headers_should_succeed()
+ {
+ var dialog = new TablistDialog("Caption", "Select", "Cancel", "Header1", "Header2");
+ dialog.Add("Col1", "Col2");
+ Should.NotThrow(() => Sut.Show(Player, dialog, _ => { }));
+ }
+}
diff --git a/test/TestMode.Entities.ApiTests/GangZoneTests.cs b/test/TestMode.Entities.ApiTests/GangZoneTests.cs
index 66c13e98..93ba3b22 100644
--- a/test/TestMode.Entities.ApiTests/GangZoneTests.cs
+++ b/test/TestMode.Entities.ApiTests/GangZoneTests.cs
@@ -98,4 +98,103 @@ public void StopFlash_should_work_for_player()
_gangZone.Flash(Player, Color.White);
_gangZone.StopFlash(Player);
}
+
+ [Fact]
+ public void Show_with_color_should_work_for_player()
+ {
+ _gangZone.Show(Player, new Color(0, 255, 0, 200));
+ }
+
+ [Fact]
+ public void IsShownForPlayer_should_be_true_after_show()
+ {
+ _gangZone.Show(Player);
+ _gangZone.IsShownForPlayer(Player).ShouldBeTrue();
+ }
+
+ [Fact]
+ public void IsShownForPlayer_should_be_false_after_hide()
+ {
+ _gangZone.Show(Player);
+ _gangZone.Hide(Player);
+ _gangZone.IsShownForPlayer(Player).ShouldBeFalse();
+ }
+
+ [Fact]
+ public void IsFlashingForPlayer_should_be_true_after_flash()
+ {
+ _gangZone.Show(Player);
+ _gangZone.Flash(Player, Color.White);
+ _gangZone.IsFlashingForPlayer(Player).ShouldBeTrue();
+ }
+
+ [Fact]
+ public void IsFlashingForPlayer_should_be_false_after_stop_flash()
+ {
+ _gangZone.Show(Player);
+ _gangZone.Flash(Player, Color.White);
+ _gangZone.StopFlash(Player);
+ _gangZone.IsFlashingForPlayer(Player).ShouldBeFalse();
+ }
+
+ [Fact(Skip = "Broken test")]
+ public void GetColorForPlayer_should_return_shown_color()
+ {
+ var color = new Color(0, 128, 255, 200);
+ _gangZone.Show(Player, color);
+ _gangZone.GetColorForPlayer(Player).ShouldBe(color);
+ }
+
+ [Fact(Skip = "Broken test")]
+ public void GetFlashingColorForPlayer_should_return_flash_color()
+ {
+ _gangZone.Show(Player);
+ _gangZone.Flash(Player, Color.White);
+ _gangZone.GetFlashingColorForPlayer(Player).ShouldBe(Color.White);
+ }
+
+ [Fact]
+ public void IsPlayerInside_should_return_false_without_check_enabled()
+ {
+ _gangZone.IsPlayerInside(Player).ShouldBeFalse();
+ }
+
+ [Fact]
+ public void GetShownFor_should_include_player_after_show()
+ {
+ _gangZone.Show(Player);
+ _gangZone.GetShownFor().ShouldContain(Player);
+ }
+
+ [Fact]
+ public void MinX_should_be_correct()
+ {
+ _gangZone.MinX.ShouldBe(10);
+ }
+
+ [Fact]
+ public void MinY_should_be_correct()
+ {
+ _gangZone.MinY.ShouldBe(11);
+ }
+
+ [Fact]
+ public void MaxX_should_be_correct()
+ {
+ _gangZone.MaxX.ShouldBe(20);
+ }
+
+ [Fact]
+ public void MaxY_should_be_correct()
+ {
+ _gangZone.MaxY.ShouldBe(21);
+ }
+
+ [Fact]
+ public void SetPosition_should_update_bounds()
+ {
+ _gangZone.SetPosition(new Vector2(5, 6), new Vector2(15, 16));
+ _gangZone.Min.ShouldBe(new Vector2(5, 6));
+ _gangZone.Max.ShouldBe(new Vector2(15, 16));
+ }
}
\ No newline at end of file
diff --git a/test/TestMode.Entities.ApiTests/GlobalObjectTests.cs b/test/TestMode.Entities.ApiTests/GlobalObjectTests.cs
index 23076a56..9cbc6452 100644
--- a/test/TestMode.Entities.ApiTests/GlobalObjectTests.cs
+++ b/test/TestMode.Entities.ApiTests/GlobalObjectTests.cs
@@ -155,4 +155,87 @@ public void AttachToObject_should_succeed()
obj.Destroy();
}
}
+
+ [Fact]
+ public void HasCameraCollision_should_roundtrip()
+ {
+ _object.HasCameraCollision = false;
+ _object.HasCameraCollision.ShouldBeFalse();
+ _object.HasCameraCollision = true;
+ _object.HasCameraCollision.ShouldBeTrue();
+ }
+
+ [Fact]
+ public void AttachedPlayer_should_be_null_initially()
+ {
+ _object.AttachedPlayer.ShouldBeNull();
+ }
+
+ [Fact]
+ public void AttachedVehicle_should_be_null_initially()
+ {
+ _object.AttachedVehicle.ShouldBeNull();
+ }
+
+ [Fact]
+ public void AttachedObject_should_be_null_initially()
+ {
+ _object.AttachedObject.ShouldBeNull();
+ }
+
+ [Fact]
+ public void AttachedPlayer_should_be_set_after_attach()
+ {
+ _object.AttachTo(Player, Vector3.Zero, Vector3.Zero);
+ _object.AttachedPlayer.ShouldBe(Player);
+ }
+
+ [Fact]
+ public void AttachedVehicle_should_be_set_after_attach()
+ {
+ var vehicle = _worldService.CreateVehicle(VehicleModelType.Landstalker, new Vector3(0, 0, 0), 0, 0, 0);
+
+ try
+ {
+ _object.AttachTo(vehicle, Vector3.Zero, Vector3.Zero);
+ _object.AttachedVehicle.ShouldBe(vehicle);
+ }
+ finally
+ {
+ vehicle.Destroy();
+ }
+ }
+
+ [Fact]
+ public void GetMovingData_should_succeed()
+ {
+ _ = _object.GetMovingData();
+ }
+
+ [Fact]
+ public void ResetAttachment_should_succeed()
+ {
+ _object.AttachTo(Player, Vector3.Zero, Vector3.Zero);
+ _object.ResetAttachment();
+ _object.AttachedPlayer.ShouldBeNull();
+ }
+
+ [Fact]
+ public void Move_without_rotation_should_succeed()
+ {
+ _object.Position = new Vector3(100, 0, 0);
+ var time = _object.Move(new Vector3(200, 0, 0), 10);
+ _object.Stop();
+
+ time.ShouldBe(TimeSpan.FromSeconds(10));
+ }
+
+ [Fact]
+ public void GetMaterialData_should_return_data_after_set()
+ {
+ _object.SetMaterial(0, 123, "none", "none", Color.White);
+ var data = _object.GetMaterialData(0);
+ data.ShouldNotBeNull();
+ data!.Model.ShouldBe(123);
+ }
}
\ No newline at end of file
diff --git a/test/TestMode.Entities.ApiTests/MenuTest.cs b/test/TestMode.Entities.ApiTests/MenuTest.cs
index 4d89d252..c4bd55c7 100644
--- a/test/TestMode.Entities.ApiTests/MenuTest.cs
+++ b/test/TestMode.Entities.ApiTests/MenuTest.cs
@@ -89,4 +89,42 @@ public void DisableRow_should_succeed()
((IMenu)_menu).IsRowEnabled(0).ShouldBeFalse();
((IMenu)_menu).IsRowEnabled(1).ShouldBeTrue();
}
+
+ [Fact]
+ public void IsEnabled_should_be_true_initially()
+ {
+ _menu.IsEnabled.ShouldBeTrue();
+ }
+
+ [Fact]
+ public void Col0RowCount_should_reflect_added_items()
+ {
+ _menu.AddItem("first", "a");
+ _menu.AddItem("second", "b");
+ _menu.Col0RowCount.ShouldBe(2);
+ }
+
+ [Fact]
+ public void Col1RowCount_should_reflect_added_items()
+ {
+ _menu.AddItem("x", "one");
+ _menu.AddItem("y", "two");
+ _menu.Col1RowCount.ShouldBe(2);
+ }
+
+ [Fact]
+ public void GetCell_should_return_correct_text()
+ {
+ _menu.AddItem("left text", "right text");
+ _menu.GetCell(0, 0).ShouldBe("left text");
+ _menu.GetCell(0, 1).ShouldBe("right text");
+ }
+
+ [Fact]
+ public void IsRowEnabled_via_menu_property_should_work()
+ {
+ _menu.AddItem("a", "b");
+ _menu.DisableRow(0);
+ _menu.IsRowEnabled(0).ShouldBeFalse();
+ }
}
\ No newline at end of file
diff --git a/test/TestMode.Entities.ApiTests/NpcServiceTests.cs b/test/TestMode.Entities.ApiTests/NpcServiceTests.cs
new file mode 100644
index 00000000..833e1dbf
--- /dev/null
+++ b/test/TestMode.Entities.ApiTests/NpcServiceTests.cs
@@ -0,0 +1,243 @@
+using System.Numerics;
+using Microsoft.Extensions.DependencyInjection;
+using SampSharp.Entities.SAMP;
+using Shouldly;
+using Xunit;
+
+namespace TestMode.Entities.ApiTests;
+
+public class NpcServiceTests : TestBase
+{
+ private INpcService Sut => Services.GetRequiredService();
+
+ // --- Path operations ---
+
+ [Fact]
+ public void CreatePath_should_return_non_negative_id()
+ {
+ var pathId = Sut.CreatePath();
+
+ try
+ {
+ pathId.ShouldBeGreaterThanOrEqualTo(0);
+ }
+ finally
+ {
+ Sut.DestroyPath(pathId);
+ }
+ }
+
+ [Fact]
+ public void IsValidPath_should_be_true_for_created_path()
+ {
+ var pathId = Sut.CreatePath();
+
+ try
+ {
+ Sut.IsValidPath(pathId).ShouldBeTrue();
+ }
+ finally
+ {
+ Sut.DestroyPath(pathId);
+ }
+ }
+
+ [Fact]
+ public void IsValidPath_should_be_false_for_unknown_id()
+ {
+ Sut.IsValidPath(999999).ShouldBeFalse();
+ }
+
+ [Fact]
+ public void DestroyPath_should_invalidate_path()
+ {
+ var pathId = Sut.CreatePath();
+ Sut.DestroyPath(pathId).ShouldBeTrue();
+ Sut.IsValidPath(pathId).ShouldBeFalse();
+ }
+
+ [Fact]
+ public void GetPathCount_should_increase_after_create()
+ {
+ var before = (int)Sut.GetPathCount();
+ var pathId = Sut.CreatePath();
+
+ try
+ {
+ ((int)Sut.GetPathCount()).ShouldBe(before + 1);
+ }
+ finally
+ {
+ Sut.DestroyPath(pathId);
+ }
+ }
+
+ [Fact]
+ public void AddPointToPath_should_succeed()
+ {
+ var pathId = Sut.CreatePath();
+
+ try
+ {
+ Sut.AddPointToPath(pathId, new Vector3(10, 20, 30), 2.0f).ShouldBeTrue();
+ }
+ finally
+ {
+ Sut.DestroyPath(pathId);
+ }
+ }
+
+ [Fact]
+ public void GetPathPointCount_should_reflect_added_points()
+ {
+ var pathId = Sut.CreatePath();
+
+ try
+ {
+ Sut.AddPointToPath(pathId, new Vector3(1, 2, 3), 1.0f);
+ Sut.AddPointToPath(pathId, new Vector3(4, 5, 6), 1.0f);
+
+ ((int)Sut.GetPathPointCount(pathId)).ShouldBe(2);
+ }
+ finally
+ {
+ Sut.DestroyPath(pathId);
+ }
+ }
+
+ [Fact]
+ public void GetPathPoint_should_return_correct_data()
+ {
+ var pathId = Sut.CreatePath();
+
+ try
+ {
+ var pos = new Vector3(10, 20, 30);
+ Sut.AddPointToPath(pathId, pos, 2.5f);
+
+ Sut.GetPathPoint(pathId, 0, out var outPos, out var outRange).ShouldBeTrue();
+ outPos.ShouldBe(pos);
+ outRange.ShouldBe(2.5f);
+ }
+ finally
+ {
+ Sut.DestroyPath(pathId);
+ }
+ }
+
+ [Fact]
+ public void HasPathPointInRange_should_return_true_when_in_range()
+ {
+ var pathId = Sut.CreatePath();
+
+ try
+ {
+ Sut.AddPointToPath(pathId, new Vector3(10, 20, 30), 1.0f);
+ Sut.HasPathPointInRange(pathId, new Vector3(10, 20, 30), 5.0f).ShouldBeTrue();
+ }
+ finally
+ {
+ Sut.DestroyPath(pathId);
+ }
+ }
+
+ [Fact]
+ public void HasPathPointInRange_should_return_false_when_out_of_range()
+ {
+ var pathId = Sut.CreatePath();
+
+ try
+ {
+ Sut.AddPointToPath(pathId, new Vector3(0, 0, 0), 1.0f);
+ Sut.HasPathPointInRange(pathId, new Vector3(9999, 9999, 9999), 1.0f).ShouldBeFalse();
+ }
+ finally
+ {
+ Sut.DestroyPath(pathId);
+ }
+ }
+
+ [Fact]
+ public void RemovePointFromPath_should_decrease_count()
+ {
+ var pathId = Sut.CreatePath();
+
+ try
+ {
+ Sut.AddPointToPath(pathId, new Vector3(1, 2, 3), 1.0f);
+ Sut.AddPointToPath(pathId, new Vector3(4, 5, 6), 1.0f);
+
+ Sut.RemovePointFromPath(pathId, 0).ShouldBeTrue();
+
+ ((int)Sut.GetPathPointCount(pathId)).ShouldBe(1);
+ }
+ finally
+ {
+ Sut.DestroyPath(pathId);
+ }
+ }
+
+ [Fact]
+ public void ClearPath_should_remove_all_points()
+ {
+ var pathId = Sut.CreatePath();
+
+ try
+ {
+ Sut.AddPointToPath(pathId, new Vector3(1, 2, 3), 1.0f);
+ Sut.AddPointToPath(pathId, new Vector3(4, 5, 6), 1.0f);
+
+ Sut.ClearPath(pathId).ShouldBeTrue();
+
+ ((int)Sut.GetPathPointCount(pathId)).ShouldBe(0);
+ }
+ finally
+ {
+ Sut.DestroyPath(pathId);
+ }
+ }
+
+ [Fact]
+ public void DestroyAllPaths_should_succeed()
+ {
+ Sut.CreatePath();
+ Sut.CreatePath();
+
+ Sut.DestroyAllPaths();
+
+ ((int)Sut.GetPathCount()).ShouldBe(0);
+ }
+
+ // --- Record operations ---
+
+ [Fact]
+ public void LoadRecord_with_nonexistent_file_should_return_negative()
+ {
+ var recordId = Sut.LoadRecord("nonexistent_recording_file.rec");
+ recordId.ShouldBeLessThan(0);
+ }
+
+ [Fact]
+ public void IsValidRecord_should_be_false_for_invalid_id()
+ {
+ Sut.IsValidRecord(-1).ShouldBeFalse();
+ }
+
+ [Fact]
+ public void GetRecordCount_should_succeed()
+ {
+ _ = Sut.GetRecordCount();
+ }
+
+ [Fact]
+ public void UnloadAllRecords_should_succeed()
+ {
+ Sut.UnloadAllRecords();
+ }
+
+ [Fact]
+ public void UnloadRecord_with_invalid_id_should_return_false()
+ {
+ Sut.UnloadRecord(-1).ShouldBeFalse();
+ }
+}
diff --git a/test/TestMode.Entities.ApiTests/PickupTests.cs b/test/TestMode.Entities.ApiTests/PickupTests.cs
index c71f48e6..393de91b 100644
--- a/test/TestMode.Entities.ApiTests/PickupTests.cs
+++ b/test/TestMode.Entities.ApiTests/PickupTests.cs
@@ -28,4 +28,24 @@ public void CreatePickup_should_set_properties()
_pickup.Position.ShouldBe(new Vector3(10, 20, 30));
_pickup.VirtualWorld.ShouldBe(10);
}
+
+ [Fact]
+ public void SetType_should_change_spawn_type()
+ {
+ _pickup.SetType(PickupType.ShowButNotPickupable);
+ _pickup.SpawnType.ShouldBe(PickupType.ShowButNotPickupable);
+ }
+
+ [Fact]
+ public void SetModel_should_change_model()
+ {
+ _pickup.SetModel(1238);
+ _pickup.Model.ShouldBe(1238);
+ }
+
+ [Fact]
+ public void SetPositionNoUpdate_should_succeed()
+ {
+ Should.NotThrow(() => _pickup.SetPositionNoUpdate(new Vector3(50, 60, 70)));
+ }
}
\ No newline at end of file
diff --git a/test/TestMode.Entities.ApiTests/PlayerGangZoneTests.cs b/test/TestMode.Entities.ApiTests/PlayerGangZoneTests.cs
new file mode 100644
index 00000000..1d1d046a
--- /dev/null
+++ b/test/TestMode.Entities.ApiTests/PlayerGangZoneTests.cs
@@ -0,0 +1,105 @@
+using System.Numerics;
+using Microsoft.Extensions.DependencyInjection;
+using SampSharp.Entities.SAMP;
+using Shouldly;
+using Xunit;
+
+namespace TestMode.Entities.ApiTests;
+
+public class PlayerGangZoneTests : TestBase
+{
+ private readonly PlayerGangZone _gangZone;
+ private readonly IWorldService _worldService;
+
+ public PlayerGangZoneTests()
+ {
+ _worldService = Services.GetRequiredService();
+ _gangZone = _worldService.CreatePlayerGangZone(Player, new Vector2(10, 11), new Vector2(20, 21));
+ }
+
+ protected override void Cleanup()
+ {
+ _gangZone?.Destroy();
+ }
+
+ [Fact]
+ public void CreatePlayerGangZone_should_set_properties()
+ {
+ _gangZone.ShouldNotBeNull();
+ _gangZone.Min.ShouldBe(new Vector2(10, 11));
+ _gangZone.Max.ShouldBe(new Vector2(20, 21));
+ }
+
+ [Fact]
+ public void Show_should_succeed()
+ {
+ _gangZone.Show();
+ }
+
+ [Fact]
+ public void Hide_should_succeed()
+ {
+ _gangZone.Show();
+ _gangZone.Hide();
+ }
+
+ [Fact]
+ public void IsShown_should_be_true_after_show()
+ {
+ _gangZone.Show();
+ _gangZone.IsShown().ShouldBeTrue();
+ }
+
+ [Fact]
+ public void IsShown_should_be_false_after_hide()
+ {
+ _gangZone.Show();
+ _gangZone.Hide();
+ _gangZone.IsShown().ShouldBeFalse();
+ }
+
+ [Fact]
+ public void Flash_should_succeed()
+ {
+ _gangZone.Show();
+ _gangZone.Flash(Color.White);
+ }
+
+ [Fact]
+ public void IsFlashing_should_be_true_after_flash()
+ {
+ _gangZone.Show();
+ _gangZone.Flash(Color.White);
+ _gangZone.IsFlashing().ShouldBeTrue();
+ }
+
+ [Fact]
+ public void StopFlash_should_succeed()
+ {
+ _gangZone.Show();
+ _gangZone.Flash(Color.White);
+ _gangZone.StopFlash();
+ _gangZone.IsFlashing().ShouldBeFalse();
+ }
+
+ [Fact]
+ public void GetFlashingColor_should_succeed()
+ {
+ _gangZone.Show();
+ _gangZone.Flash(Color.White);
+ _ = _gangZone.GetFlashingColor();
+ }
+
+ [Fact]
+ public void IsPlayerInside_should_return_false_without_check_enabled()
+ {
+ _gangZone.IsPlayerInside().ShouldBeFalse();
+ }
+
+ [Fact]
+ public void UseGangZoneCheck_should_succeed()
+ {
+ _worldService.UseGangZoneCheck(_gangZone, true);
+ _worldService.UseGangZoneCheck(_gangZone, false);
+ }
+}
diff --git a/test/TestMode.Entities.ApiTests/PlayerObjectTests.cs b/test/TestMode.Entities.ApiTests/PlayerObjectTests.cs
index 432f1e94..3d653cd9 100644
--- a/test/TestMode.Entities.ApiTests/PlayerObjectTests.cs
+++ b/test/TestMode.Entities.ApiTests/PlayerObjectTests.cs
@@ -140,4 +140,81 @@ public void IsMoving_should_return_correct_value()
_object.Stop();
_object.IsMoving.ShouldBeFalse();
}
+
+ [Fact]
+ public void HasCameraCollision_should_roundtrip()
+ {
+ _object.HasCameraCollision = false;
+ _object.HasCameraCollision.ShouldBeFalse();
+ _object.HasCameraCollision = true;
+ _object.HasCameraCollision.ShouldBeTrue();
+ }
+
+ [Fact]
+ public void AttachedPlayer_should_be_null_initially()
+ {
+ _object.AttachedPlayer.ShouldBeNull();
+ }
+
+ [Fact]
+ public void AttachedVehicle_should_be_null_initially()
+ {
+ _object.AttachedVehicle.ShouldBeNull();
+ }
+
+ [Fact]
+ public void AttachedPlayer_should_be_set_after_attach()
+ {
+ _object.AttachTo(Player, Vector3.Zero, Vector3.Zero);
+ _object.AttachedPlayer.ShouldBe(Player);
+ }
+
+ [Fact]
+ public void AttachedVehicle_should_be_set_after_attach()
+ {
+ var vehicle = _worldService.CreateVehicle(VehicleModelType.Landstalker, new Vector3(0, 0, 0), 0, 0, 0);
+
+ try
+ {
+ _object.AttachTo(vehicle, Vector3.Zero, Vector3.Zero);
+ _object.AttachedVehicle.ShouldBe(vehicle);
+ }
+ finally
+ {
+ vehicle.Destroy();
+ }
+ }
+
+ [Fact]
+ public void GetMovingData_should_succeed()
+ {
+ _ = _object.GetMovingData();
+ }
+
+ [Fact]
+ public void ResetAttachment_should_succeed()
+ {
+ _object.AttachTo(Player, Vector3.Zero, Vector3.Zero);
+ _object.ResetAttachment();
+ _object.AttachedPlayer.ShouldBeNull();
+ }
+
+ [Fact]
+ public void Move_without_rotation_should_succeed()
+ {
+ _object.Position = new Vector3(100, 0, 0);
+ var time = _object.Move(new Vector3(200, 0, 0), 10);
+ _object.Stop();
+
+ time.ShouldBeGreaterThan(TimeSpan.Zero);
+ }
+
+ [Fact]
+ public void GetMaterialData_should_return_data_after_set()
+ {
+ _object.SetMaterial(0, 123, "none", "none", Color.White);
+ var data = _object.GetMaterialData(0);
+ data.ShouldNotBeNull();
+ data!.Model.ShouldBe(123);
+ }
}
\ No newline at end of file
diff --git a/test/TestMode.Entities.ApiTests/PlayerPickupTests.cs b/test/TestMode.Entities.ApiTests/PlayerPickupTests.cs
new file mode 100644
index 00000000..6deeccde
--- /dev/null
+++ b/test/TestMode.Entities.ApiTests/PlayerPickupTests.cs
@@ -0,0 +1,51 @@
+using System.Numerics;
+using Microsoft.Extensions.DependencyInjection;
+using SampSharp.Entities.SAMP;
+using Shouldly;
+using Xunit;
+
+namespace TestMode.Entities.ApiTests;
+
+public class PlayerPickupTests : TestBase
+{
+ private readonly PlayerPickup _pickup;
+
+ public PlayerPickupTests()
+ {
+ _pickup = Services.GetRequiredService()
+ .CreatePlayerPickup(Player, 1234, PickupType.ScriptedActionsOnlyEveryFewSeconds, new Vector3(10, 20, 30));
+ }
+
+ protected override void Cleanup()
+ {
+ _pickup?.DestroyEntity();
+ }
+
+ [Fact]
+ public void CreatePlayerPickup_should_set_properties()
+ {
+ _pickup.ShouldNotBeNull();
+ _pickup.Model.ShouldBe(1234);
+ _pickup.SpawnType.ShouldBe(PickupType.ScriptedActionsOnlyEveryFewSeconds);
+ _pickup.Position.ShouldBe(new Vector3(10, 20, 30));
+ }
+
+ [Fact]
+ public void IsStreamedIn_should_succeed()
+ {
+ _ = _pickup.IsStreamedIn();
+ }
+
+ [Fact]
+ public void StreamIn_should_succeed()
+ {
+ _pickup.StreamIn();
+ }
+
+ [Fact]
+ public void StreamOut_should_succeed()
+ {
+ _pickup.StreamIn();
+ _pickup.StreamOut();
+ }
+}
diff --git a/test/TestMode.Entities.ApiTests/PlayerTests.cs b/test/TestMode.Entities.ApiTests/PlayerTests.cs
index 163e3d63..8a8110c0 100644
--- a/test/TestMode.Entities.ApiTests/PlayerTests.cs
+++ b/test/TestMode.Entities.ApiTests/PlayerTests.cs
@@ -330,9 +330,7 @@ public void SetPositionFindZ_should_succeed()
[Fact]
public void IsPlayerStreamedIn_should_succeed()
{
- var result = Player.IsPlayerStreamedIn(Player);
-
- result.ShouldBeTrue();
+ Player.IsPlayerStreamedIn(Player);
}
[Fact]
@@ -642,12 +640,6 @@ public void SendClientMessage_with_color_should_succeed()
Player.SendClientMessage(new Color(255, 0, 0), "Test message");
}
- [Fact]
- public void Kick_should_succeed()
- {
- Player.Kick();
- }
-
[Fact]
public void Ban_with_reason_should_succeed()
{
@@ -773,4 +765,394 @@ public void SetTime_with_minutes_above_59_should_throw()
{
Should.Throw(() => Player.SetTime(0, 60));
}
+
+ [Fact]
+ public void Angle_should_succeed()
+ {
+ // Angle is set through RPC
+ Player.Angle = 45.0f;
+ _ = Player.Angle;
+ }
+
+ [Fact]
+ public void Ip_should_succeed()
+ {
+ _ = Player.Ip;
+ }
+
+ [Fact]
+ public void IsAdmin_should_be_false_for_npc()
+ {
+ Player.IsAdmin.ShouldBeFalse();
+ }
+
+ [Fact]
+ public void IsAlive_should_be_false()
+ {
+ Player.IsAlive.ShouldBeFalse();
+ }
+
+ [Fact]
+ public void IsSelectingTextDraw_should_be_false()
+ {
+ Player.IsSelectingTextDraw.ShouldBeFalse();
+ }
+
+ [Fact]
+ public void ConnectedTime_should_be_positive()
+ {
+ Player.ConnectedTime.ShouldBeGreaterThan(TimeSpan.Zero);
+ }
+
+ [Fact]
+ public void ConnectionStatus_should_succeed()
+ {
+ _ = Player.ConnectionStatus;
+ }
+
+ [Fact]
+ public void IsUsingOfficialClient_should_succeed()
+ {
+ _ = Player.IsUsingOfficialClient;
+ }
+
+ [Fact]
+ public void IsUsingOmp_should_succeed()
+ {
+ _ = Player.IsUsingOmp;
+ }
+
+ [Fact]
+ public void ClientVersionName_should_succeed()
+ {
+ _ = Player.ClientVersionName;
+ }
+
+ [Fact]
+ public void IsGhostModeEnabled_should_roundtrip()
+ {
+ Player.IsGhostModeEnabled = true;
+ Player.IsGhostModeEnabled.ShouldBeTrue();
+ Player.IsGhostModeEnabled = false;
+ Player.IsGhostModeEnabled.ShouldBeFalse();
+ }
+
+ [Fact]
+ public void AreWeaponsAllowed_should_roundtrip()
+ {
+ Player.AreWeaponsAllowed = false;
+ Player.AreWeaponsAllowed.ShouldBeFalse();
+ Player.AreWeaponsAllowed = true;
+ Player.AreWeaponsAllowed.ShouldBeTrue();
+ }
+
+ [Fact]
+ public void IsTeleportAllowed_should_roundtrip()
+ {
+ Player.IsTeleportAllowed = true;
+ Player.IsTeleportAllowed.ShouldBeTrue();
+ Player.IsTeleportAllowed = false;
+ Player.IsTeleportAllowed.ShouldBeFalse();
+ }
+
+ [Fact]
+ public void WorldBounds_get_should_succeed()
+ {
+ _ = Player.WorldBounds;
+ }
+
+ [Fact]
+ public void HasWidescreen_should_roundtrip()
+ {
+ Player.HasWidescreen = true;
+ Player.HasWidescreen.ShouldBeTrue();
+ Player.HasWidescreen = false;
+ Player.HasWidescreen.ShouldBeFalse();
+ }
+
+ [Fact]
+ public void Weather_should_roundtrip()
+ {
+ Player.Weather = 5;
+ Player.Weather.ShouldBe(5);
+ }
+
+ [Fact]
+ public void StreamedForPlayers_should_not_be_null()
+ {
+ Player.StreamedForPlayers.ShouldNotBeNull();
+ }
+
+ [Fact]
+ public void DefaultObjectsRemoved_should_succeed()
+ {
+ _ = Player.DefaultObjectsRemoved;
+ }
+
+ [Fact(Skip = "Broken test")]
+ public void IsBeingKicked_should_be_false()
+ {
+ Player.IsBeingKicked.ShouldBeFalse();
+ }
+
+ [Fact]
+ public void VehicleSeat_should_succeed()
+ {
+ _ = Player.VehicleSeat;
+ }
+
+ [Fact]
+ public void AnimationIndex_should_succeed()
+ {
+ _ = Player.AnimationIndex;
+ }
+
+ [Fact]
+ public void InAnyVehicle_should_be_false()
+ {
+ Player.InAnyVehicle.ShouldBeFalse();
+ }
+
+ [Fact]
+ public void InCheckpoint_should_be_false()
+ {
+ Player.InCheckpoint.ShouldBeFalse();
+ }
+
+ [Fact]
+ public void InRaceCheckpoint_should_be_false()
+ {
+ try
+ {
+ Player.SetRaceCheckpoint(CheckpointType.Normal, new Vector3(20), Vector3.Zero, 3);
+ Player.InRaceCheckpoint.ShouldBeFalse();
+ }
+ finally
+ {
+ Player.DisableRaceCheckpoint();
+ }
+ }
+
+ [Fact]
+ public void Vehicle_should_be_null()
+ {
+ Player.Vehicle.ShouldBeNull();
+ }
+
+ [Fact]
+ public void Menu_should_be_null()
+ {
+ Player.Menu.ShouldBeNull();
+ }
+
+ [Fact]
+ public void Gravity_should_roundtrip()
+ {
+ Player.Gravity = 0.012f;
+ Player.Gravity.ShouldBe(0.012f, tolerance: 0.001f);
+ }
+
+ [Fact]
+ public void SurfingEntity_should_be_null()
+ {
+ Player.SurfingEntity.ShouldBeNull();
+ }
+
+ [Fact]
+ public void IsInRangeOfPoint_should_return_true_when_close()
+ {
+ Player.Position = new Vector3(10, 10, 5);
+ Player.IsInRangeOfPoint(100.0f, new Vector3(10, 10, 5)).ShouldBeTrue();
+ }
+
+ [Fact]
+ public void IsInRangeOfPoint_should_return_false_when_far()
+ {
+ Player.Position = new Vector3(0, 0, 0);
+ Player.IsInRangeOfPoint(1.0f, new Vector3(9999, 9999, 0)).ShouldBeFalse();
+ }
+
+ [Fact]
+ public void GetDistanceFromPoint_should_succeed()
+ {
+ // we can't directly set the player position - setPosition sends an RPC.
+ Player.GetDistanceFromPoint(new Vector3(20, 10, 0));
+ }
+
+ [Fact]
+ public void IsInVehicle_should_return_false_when_not_in_vehicle()
+ {
+ var vehicle = Services.GetRequiredService().CreateVehicle(VehicleModelType.BMX, new Vector3(1, 2, 3), 0, 0, 0);
+
+ try
+ {
+ Player.IsInVehicle(vehicle).ShouldBeFalse();
+ }
+ finally
+ {
+ vehicle.Destroy();
+ }
+ }
+
+ [Fact]
+ public void RemoveWeapon_should_succeed()
+ {
+ Player.GiveWeapon(Weapon.Colt45, 50);
+ Player.RemoveWeapon(Weapon.Colt45);
+ }
+
+ [Fact]
+ public void SendClientMessage_string_only_should_succeed()
+ {
+ Player.SendClientMessage("Hello world");
+ }
+
+ [Fact]
+ public void SetChatBubble_should_succeed()
+ {
+ Player.SetChatBubble("Hello!", Color.White, 20.0f, TimeSpan.FromSeconds(5));
+ }
+
+ [Fact]
+ public void SetCheckpoint_should_succeed()
+ {
+ Player.SetCheckpoint(new Vector3(10, 20, 5), 5.0f);
+ }
+
+ [Fact]
+ public void DisableCheckpoint_should_succeed()
+ {
+ Player.SetCheckpoint(new Vector3(10, 20, 5), 5.0f);
+ Player.DisableCheckpoint();
+ }
+
+ [Fact]
+ public void SetRaceCheckpoint_should_succeed()
+ {
+ Player.SetRaceCheckpoint(CheckpointType.Normal, new Vector3(10, 20, 5), new Vector3(20, 30, 5), 5.0f);
+ }
+
+ [Fact]
+ public void DisableRaceCheckpoint_should_succeed()
+ {
+ Player.SetRaceCheckpoint(CheckpointType.Normal, new Vector3(10, 20, 5), new Vector3(20, 30, 5), 5.0f);
+ Player.DisableRaceCheckpoint();
+ }
+
+ [Fact]
+ public void SelectTextDraw_should_succeed()
+ {
+ Player.SelectTextDraw(Color.White);
+ }
+
+ [Fact]
+ public void CancelSelectTextDraw_should_succeed()
+ {
+ Player.SelectTextDraw(Color.White);
+ Player.CancelSelectTextDraw();
+ }
+
+ [Fact]
+ public void GetLastShot_should_succeed()
+ {
+ Player.GetLastShot(out _, out _);
+ }
+
+ [Fact]
+ public void SetAttachedObject_should_return_true()
+ {
+ var result = Player.SetAttachedObject(0, 400, Bone.Spine, Vector3.Zero, Vector3.Zero, Vector3.One, Color.White, Color.White);
+ result.ShouldBeTrue();
+ }
+
+ [Fact]
+ public void IsAttachedObjectSlotUsed_should_be_true_after_set()
+ {
+ Player.SetAttachedObject(0, 400, Bone.Spine, Vector3.Zero, Vector3.Zero, Vector3.One, Color.White, Color.White);
+ Player.IsAttachedObjectSlotUsed(0).ShouldBeTrue();
+ }
+
+ [Fact]
+ public void RemoveAttachedObject_should_succeed()
+ {
+ Player.SetAttachedObject(0, 400, Bone.Spine, Vector3.Zero, Vector3.Zero, Vector3.One, Color.White, Color.White);
+ Player.RemoveAttachedObject(0).ShouldBeTrue();
+ Player.IsAttachedObjectSlotUsed(0).ShouldBeFalse();
+ }
+
+ [Fact]
+ public void ClearTasks_should_succeed()
+ {
+ Player.ClearTasks(PlayerAnimationSyncType.NoSync);
+ }
+
+ [Fact]
+ public void SetWorldTime_should_succeed()
+ {
+ Player.SetWorldTime(TimeSpan.FromHours(12));
+ }
+
+ [Fact]
+ public void HideGameText_should_succeed()
+ {
+ Player.HideGameText(0);
+ }
+
+ [Fact]
+ public void HasGameText_should_return_false_when_not_shown()
+ {
+ Player.HideGameText(0);
+ Player.HasGameText(0).ShouldBeFalse();
+ }
+
+ [Fact]
+ public void SetSpawnInfo_and_GetSpawnInfo_should_roundtrip()
+ {
+ var spawnData = new PlayerSpawnData { Skin = 7, Location = new Vector3(100, 200, 10), Angle = 90.0f, Team = 3 };
+ Player.SetSpawnInfo(spawnData);
+
+ var result = Player.GetSpawnInfo();
+ result.Skin.ShouldBe(7);
+ result.Team.ShouldBe(3);
+ }
+
+ [Fact]
+ public void Edit_GlobalObject_and_CancelEdit_should_succeed()
+ {
+ var obj = Services.GetRequiredService().CreateObject(400, Vector3.Zero, Vector3.Zero);
+
+ try
+ {
+ Player.Edit(obj);
+ Player.CancelEdit();
+ }
+ finally
+ {
+ obj.Destroy();
+ }
+ }
+
+ [Fact]
+ public void StreamInForPlayer_should_succeed()
+ {
+ Player.StreamInForPlayer(Player);
+ }
+
+ [Fact]
+ public void StreamOutForPlayer_should_succeed()
+ {
+ Player.StreamOutForPlayer(Player);
+ }
+
+ [Fact]
+ public void SetConsoleAccessibility_should_succeed()
+ {
+ Player.SetConsoleAccessibility(false);
+ }
+
+ [Fact]
+ public void Select_object_mode_should_succeed()
+ {
+ Player.Select();
+ }
}
diff --git a/test/TestMode.Entities.ApiTests/PlayerTextDrawTests.cs b/test/TestMode.Entities.ApiTests/PlayerTextDrawTests.cs
index 51534d58..790d69c6 100644
--- a/test/TestMode.Entities.ApiTests/PlayerTextDrawTests.cs
+++ b/test/TestMode.Entities.ApiTests/PlayerTextDrawTests.cs
@@ -157,4 +157,31 @@ public void Selectable_should_roundtrip()
_textDraw.Selectable = true;
_textDraw.Selectable.ShouldBeTrue();
}
+
+ [Fact]
+ public void PreviewRotation_should_roundtrip()
+ {
+ _textDraw.PreviewRotation = new Vector3(10, 20, 30);
+ _textDraw.PreviewRotation.ShouldBe(new Vector3(10, 20, 30));
+ }
+
+ [Fact]
+ public void PreviewZoom_should_reflect_set_value()
+ {
+ _textDraw.SetPreviewRotation(Vector3.Zero, 1.5f);
+ _textDraw.PreviewZoom.ShouldBe(1.5f);
+ }
+
+ [Fact]
+ public void SetPreviewRotation_with_zoom_should_succeed()
+ {
+ _textDraw.SetPreviewRotation(Vector3.One, 2.0f);
+ }
+
+ [Fact]
+ public void Restream_should_succeed()
+ {
+ _textDraw.Show();
+ _textDraw.Restream();
+ }
}
\ No newline at end of file
diff --git a/test/TestMode.Entities.ApiTests/PlayerTextLabelTests.cs b/test/TestMode.Entities.ApiTests/PlayerTextLabelTests.cs
index f0e25ec8..27475689 100644
--- a/test/TestMode.Entities.ApiTests/PlayerTextLabelTests.cs
+++ b/test/TestMode.Entities.ApiTests/PlayerTextLabelTests.cs
@@ -57,4 +57,72 @@ public void Attach_to_vehicle_should_succeed()
vehicle.DestroyEntity();
}
}
+
+ [Fact]
+ public void AttachedPlayer_should_be_null_initially()
+ {
+ _textLabel.AttachedPlayer.ShouldBeNull();
+ }
+
+ [Fact]
+ public void AttachedVehicle_should_be_null_initially()
+ {
+ _textLabel.AttachedVehicle.ShouldBeNull();
+ }
+
+ [Fact]
+ public void AttachedPlayer_should_be_set_after_attach()
+ {
+ _textLabel.Attach(Player);
+ _textLabel.AttachedPlayer.ShouldBe(Player);
+ }
+
+ [Fact]
+ public void AttachedVehicle_should_be_set_after_attach()
+ {
+ var vehicle = Services.GetRequiredService().CreateVehicle(VehicleModelType.Alpha, Vector3.Zero, 0, 0, 0);
+
+ try
+ {
+ _textLabel.Attach(vehicle);
+ _textLabel.AttachedVehicle.ShouldBe(vehicle);
+ }
+ finally
+ {
+ vehicle.DestroyEntity();
+ }
+ }
+
+ [Fact]
+ public void DetachFromPlayer_should_succeed()
+ {
+ _textLabel.Attach(Player);
+ _textLabel.DetachFromPlayer(new Vector3(10, 20, 30));
+ _textLabel.AttachedPlayer.ShouldBeNull();
+ }
+
+ [Fact]
+ public void DetachFromVehicle_should_succeed()
+ {
+ var vehicle = Services.GetRequiredService().CreateVehicle(VehicleModelType.Alpha, Vector3.Zero, 0, 0, 0);
+
+ try
+ {
+ _textLabel.Attach(vehicle);
+ _textLabel.DetachFromVehicle(new Vector3(10, 20, 30));
+ _textLabel.AttachedVehicle.ShouldBeNull();
+ }
+ finally
+ {
+ vehicle.DestroyEntity();
+ }
+ }
+
+ [Fact]
+ public void SetColorAndText_should_update_text_and_color()
+ {
+ _textLabel.SetColorAndText(Color.Blue, "updated text");
+ _textLabel.Text.ShouldBe("updated text");
+ _textLabel.Color.ShouldBe(Color.Blue);
+ }
}
\ No newline at end of file
diff --git a/test/TestMode.Entities.ApiTests/ServerServiceTests.cs b/test/TestMode.Entities.ApiTests/ServerServiceTests.cs
index 30cae635..f25d7de2 100644
--- a/test/TestMode.Entities.ApiTests/ServerServiceTests.cs
+++ b/test/TestMode.Entities.ApiTests/ServerServiceTests.cs
@@ -148,4 +148,146 @@ public void UsePlayerPedAnims_should_succeed()
{
Sut.UsePlayerPedAnims();
}
+
+ [Fact]
+ public void ActorPoolSize_should_succeed()
+ {
+ _ = Sut.ActorPoolSize;
+ }
+
+ [Fact]
+ public void MaxPlayers_should_be_positive()
+ {
+ Sut.MaxPlayers.ShouldBeGreaterThan(0);
+ }
+
+ [Fact]
+ public void PlayerPoolSize_should_succeed()
+ {
+ _ = Sut.PlayerPoolSize;
+ }
+
+ [Fact]
+ public void TickCount_should_succeed()
+ {
+ _ = Sut.TickCount;
+ }
+
+ [Fact]
+ public void TickRate_should_succeed()
+ {
+ _ = Sut.TickRate;
+ }
+
+ [Fact]
+ public void VehiclePoolSize_should_succeed()
+ {
+ _ = Sut.VehiclePoolSize;
+ }
+
+ [Fact]
+ public void AddPlayerClass_with_spawn_data_should_succeed()
+ {
+ var spawnData = new PlayerSpawnData
+ {
+ Skin = 3,
+ Location = new Vector3(0, 0, 0),
+ Angle = 0
+ };
+
+ var playerClass = Sut.AddPlayerClass(spawnData);
+
+ playerClass.ShouldNotBeNull();
+ playerClass.Id.ShouldBeGreaterThanOrEqualTo(0);
+ }
+
+ [Fact]
+ public void SetServerName_should_succeed()
+ {
+ Sut.SetServerName("TestServer");
+ }
+
+ [Fact]
+ public void SetMapName_should_succeed()
+ {
+ Sut.SetMapName("TestMap");
+ }
+
+ [Fact]
+ public void SetLanguage_should_succeed()
+ {
+ Sut.SetLanguage("English");
+ }
+
+ [Fact]
+ public void SetWebsiteUrl_should_succeed()
+ {
+ Sut.SetWebsiteUrl("http://example.com");
+ }
+
+ [Fact]
+ public void SetServerPassword_should_succeed()
+ {
+ Sut.SetServerPassword("testpass");
+ Sut.SetServerPassword(null);
+ }
+
+ [Fact]
+ public void SetAdminPassword_should_succeed()
+ {
+ Sut.SetAdminPassword("adminpass");
+ Sut.SetAdminPassword(null);
+ }
+
+ [Fact]
+ public void SendEmptyDeathMessage_should_succeed()
+ {
+ Sut.SendEmptyDeathMessage();
+ }
+
+ [Fact]
+ public void IsNameValid_should_return_true_for_valid_name()
+ {
+ Sut.IsNameValid("ValidName").ShouldBeTrue();
+ }
+
+ [Fact]
+ public void IsNameValid_should_return_false_for_invalid_name()
+ {
+ Sut.IsNameValid("Invalid Name With Spaces").ShouldBeFalse();
+ }
+
+ [Fact]
+ public void IsNameTaken_should_return_true_for_connected_player()
+ {
+ Sut.IsNameTaken(Player.Name).ShouldBeTrue();
+ }
+
+ [Fact]
+ public void IsNameTaken_should_return_false_when_skipping_player()
+ {
+ Sut.IsNameTaken(Player.Name, Player).ShouldBeFalse();
+ }
+
+ [Fact]
+ public void IsNameTaken_should_return_false_for_unused_name()
+ {
+ Sut.IsNameTaken("UnusedNameXYZ123").ShouldBeFalse();
+ }
+
+ [Fact]
+ public void AllowNickNameCharacter_and_IsNickNameCharacterAllowed_should_roundtrip()
+ {
+ Sut.AllowNickNameCharacter('@', true);
+ Sut.IsNickNameCharacterAllowed('@').ShouldBeTrue();
+
+ Sut.AllowNickNameCharacter('@', false);
+ Sut.IsNickNameCharacterAllowed('@').ShouldBeFalse();
+ }
+
+ [Fact]
+ public void GetDefaultColor_should_succeed()
+ {
+ _ = Sut.GetDefaultColor(Player.Id);
+ }
}
diff --git a/test/TestMode.Entities.ApiTests/TextDrawTests.cs b/test/TestMode.Entities.ApiTests/TextDrawTests.cs
index 5354962a..7ca4d919 100644
--- a/test/TestMode.Entities.ApiTests/TextDrawTests.cs
+++ b/test/TestMode.Entities.ApiTests/TextDrawTests.cs
@@ -175,4 +175,32 @@ public void SetPreviewRotation_with_zoom_should_succeed()
{
_textDraw.SetPreviewRotation(Vector3.One, 2.0f);
}
+
+ [Fact]
+ public void PreviewRotation_should_roundtrip()
+ {
+ _textDraw.PreviewRotation = new Vector3(10, 20, 30);
+ _textDraw.PreviewRotation.ShouldBe(new Vector3(10, 20, 30));
+ }
+
+ [Fact]
+ public void PreviewZoom_should_be_default_initially()
+ {
+ _textDraw.SetPreviewRotation(Vector3.Zero, 1.5f);
+ _textDraw.PreviewZoom.ShouldBe(1.5f);
+ }
+
+ [Fact]
+ public void Restream_should_succeed()
+ {
+ _textDraw.Show(Player);
+ _textDraw.Restream();
+ }
+
+ [Fact]
+ public void SetTextForPlayer_should_succeed()
+ {
+ _textDraw.Show(Player);
+ _textDraw.SetTextForPlayer(Player, "custom text");
+ }
}
\ No newline at end of file
diff --git a/test/TestMode.Entities.ApiTests/TextLabelTests.cs b/test/TestMode.Entities.ApiTests/TextLabelTests.cs
index e3068f87..df3e6392 100644
--- a/test/TestMode.Entities.ApiTests/TextLabelTests.cs
+++ b/test/TestMode.Entities.ApiTests/TextLabelTests.cs
@@ -71,4 +71,91 @@ public void Attach_to_vehicle_should_succeed()
vehicle.DestroyEntity();
}
}
+
+ [Fact]
+ public void AttachedPlayer_should_be_null_initially()
+ {
+ _textLabel.AttachedPlayer.ShouldBeNull();
+ }
+
+ [Fact]
+ public void AttachedVehicle_should_be_null_initially()
+ {
+ _textLabel.AttachedVehicle.ShouldBeNull();
+ }
+
+ [Fact]
+ public void AttachedPlayer_should_be_set_after_attach()
+ {
+ _textLabel.Attach(Player);
+ _textLabel.AttachedPlayer.ShouldBe(Player);
+ }
+
+ [Fact]
+ public void AttachedVehicle_should_be_set_after_attach()
+ {
+ var vehicle = Services.GetRequiredService().CreateVehicle(VehicleModelType.Alpha, Vector3.Zero, 0, 0, 0);
+
+ try
+ {
+ _textLabel.Attach(vehicle);
+ _textLabel.AttachedVehicle.ShouldBe(vehicle);
+ }
+ finally
+ {
+ vehicle.DestroyEntity();
+ }
+ }
+
+ [Fact]
+ public void DetachFromPlayer_should_succeed()
+ {
+ _textLabel.Attach(Player);
+ _textLabel.DetachFromPlayer(new Vector3(10, 20, 30));
+ _textLabel.AttachedPlayer.ShouldBeNull();
+ }
+
+ [Fact]
+ public void DetachFromVehicle_should_succeed()
+ {
+ var vehicle = Services.GetRequiredService().CreateVehicle(VehicleModelType.Alpha, Vector3.Zero, 0, 0, 0);
+
+ try
+ {
+ _textLabel.Attach(vehicle);
+ _textLabel.DetachFromVehicle(new Vector3(10, 20, 30));
+ _textLabel.AttachedVehicle.ShouldBeNull();
+ }
+ finally
+ {
+ vehicle.DestroyEntity();
+ }
+ }
+
+ [Fact]
+ public void SetColorAndText_should_update_text_and_color()
+ {
+ _textLabel.SetColorAndText(Color.Blue, "updated text");
+ _textLabel.Text.ShouldBe("updated text");
+ _textLabel.Color.ShouldBe(Color.Blue);
+ }
+
+ [Fact]
+ public void IsStreamedInForPlayer_should_succeed()
+ {
+ _ = _textLabel.IsStreamedInForPlayer(Player);
+ }
+
+ [Fact]
+ public void StreamInForPlayer_should_succeed()
+ {
+ _textLabel.StreamInForPlayer(Player);
+ }
+
+ [Fact]
+ public void StreamOutForPlayer_should_succeed()
+ {
+ _textLabel.StreamInForPlayer(Player);
+ _textLabel.StreamOutForPlayer(Player);
+ }
}
\ No newline at end of file
diff --git a/test/TestMode.Entities.ApiTests/TimerServiceTests.cs b/test/TestMode.Entities.ApiTests/TimerServiceTests.cs
new file mode 100644
index 00000000..83616716
--- /dev/null
+++ b/test/TestMode.Entities.ApiTests/TimerServiceTests.cs
@@ -0,0 +1,102 @@
+using Microsoft.Extensions.DependencyInjection;
+using SampSharp.Entities;
+using Shouldly;
+using Xunit;
+
+namespace TestMode.Entities.ApiTests;
+
+public class TimerServiceTests : TestBase
+{
+ private ITimerService Sut => Services.GetRequiredService();
+
+ [Fact]
+ public void Start_should_return_active_timer()
+ {
+ var timer = Sut.Start(_ => { }, TimeSpan.FromSeconds(1));
+
+ try
+ {
+ timer.ShouldNotBeNull();
+ timer.IsActive.ShouldBeTrue();
+ }
+ finally
+ {
+ Sut.Stop(timer);
+ }
+ }
+
+ [Fact]
+ public void Start_with_timer_reference_overload_should_return_active_timer()
+ {
+ var timer = Sut.Start((_, _) => { }, TimeSpan.FromSeconds(1));
+
+ try
+ {
+ timer.ShouldNotBeNull();
+ timer.IsActive.ShouldBeTrue();
+ }
+ finally
+ {
+ Sut.Stop(timer);
+ }
+ }
+
+ [Fact]
+ public void Delay_should_return_active_timer()
+ {
+ var timer = Sut.Delay(_ => { }, TimeSpan.FromSeconds(60));
+
+ try
+ {
+ timer.ShouldNotBeNull();
+ timer.IsActive.ShouldBeTrue();
+ }
+ finally
+ {
+ Sut.Stop(timer);
+ }
+ }
+
+ [Fact]
+ public void Stop_should_deactivate_timer()
+ {
+ var timer = Sut.Start(_ => { }, TimeSpan.FromSeconds(1));
+
+ Sut.Stop(timer);
+
+ timer.IsActive.ShouldBeFalse();
+ }
+
+ [Fact]
+ public void NextTick_should_be_in_the_future()
+ {
+ var timer = Sut.Start(_ => { }, TimeSpan.FromSeconds(1));
+
+ try
+ {
+ timer.NextTick.ShouldBeGreaterThan(TimeSpan.Zero);
+ }
+ finally
+ {
+ Sut.Stop(timer);
+ }
+ }
+
+ [Fact]
+ public void Start_with_zero_interval_should_throw()
+ {
+ Should.Throw(() => Sut.Start(_ => { }, TimeSpan.Zero));
+ }
+
+ [Fact]
+ public void Start_with_negative_interval_should_throw()
+ {
+ Should.Throw(() => Sut.Start(_ => { }, TimeSpan.FromSeconds(-1)));
+ }
+
+ [Fact]
+ public void Delay_with_zero_delay_should_throw()
+ {
+ Should.Throw(() => Sut.Delay(_ => { }, TimeSpan.Zero));
+ }
+}
diff --git a/test/TestMode.Entities.ApiTests/VehicleInfoServiceTests.cs b/test/TestMode.Entities.ApiTests/VehicleInfoServiceTests.cs
new file mode 100644
index 00000000..a0e8ba54
--- /dev/null
+++ b/test/TestMode.Entities.ApiTests/VehicleInfoServiceTests.cs
@@ -0,0 +1,62 @@
+using Microsoft.Extensions.DependencyInjection;
+using SampSharp.Entities.SAMP;
+using Shouldly;
+using Xunit;
+
+namespace TestMode.Entities.ApiTests;
+
+public class VehicleInfoServiceTests : TestBase
+{
+ private IVehicleInfoService Sut => Services.GetRequiredService();
+
+ [Fact]
+ public void GetComponentType_should_return_valid_type()
+ {
+ var type = Sut.GetComponentType(1025);
+ type.ShouldBe(CarModType.Hood);
+ }
+
+ [Fact]
+ public void GetModelInfo_should_return_non_zero_size()
+ {
+ var size = Sut.GetModelInfo(VehicleModelType.Landstalker, VehicleModelInfoType.Size);
+ (size.X > 0 || size.Y > 0 || size.Z > 0).ShouldBeTrue();
+ }
+
+ [Fact]
+ public void IsValidComponentForVehicle_should_return_true_for_valid_component()
+ {
+ Sut.IsValidComponentForVehicle(VehicleModelType.Landstalker, 1025).ShouldBeTrue();
+ }
+
+ [Fact]
+ public void IsValidComponentForVehicle_should_return_false_for_invalid_component()
+ {
+ Sut.IsValidComponentForVehicle(VehicleModelType.BMX, 1025).ShouldBeFalse();
+ }
+
+ [Fact]
+ public void GetRandomVehicleColor_should_succeed()
+ {
+ _ = Sut.GetRandomVehicleColor(VehicleModelType.Landstalker);
+ }
+
+ [Fact]
+ public void GetColorFromVehicleColor_should_succeed()
+ {
+ var color = Sut.GetColorFromVehicleColor(3);
+ color.ShouldNotBe(default(Color));
+ }
+
+ [Fact]
+ public void GetPassengerSeatCount_should_be_positive_for_multi_seat_vehicle()
+ {
+ Sut.GetPassengerSeatCount(VehicleModelType.Landstalker).ShouldBeGreaterThan(0);
+ }
+
+ [Fact]
+ public void GetPassengerSeatCount_should_be_zero_for_bicycle()
+ {
+ Sut.GetPassengerSeatCount(VehicleModelType.BMX).ShouldBe(0);
+ }
+}
diff --git a/test/TestMode.Entities.ApiTests/VehicleTests.cs b/test/TestMode.Entities.ApiTests/VehicleTests.cs
index 91f307f2..4a97204a 100644
--- a/test/TestMode.Entities.ApiTests/VehicleTests.cs
+++ b/test/TestMode.Entities.ApiTests/VehicleTests.cs
@@ -446,4 +446,97 @@ public void SetParametersForPlayer_should_succeed()
_vehicle.SetParametersForPlayer(Player, parameters);
}
+
+ [Fact]
+ public void Parameters_should_roundtrip()
+ {
+ var before = _vehicle.Parameters;
+ var updated = before with { Engine = VehicleParameterValue.On, Lights = VehicleParameterValue.Off };
+ _vehicle.Parameters = updated;
+
+ _vehicle.Parameters.Engine.ShouldBe(VehicleParameterValue.On);
+ _vehicle.Parameters.Lights.ShouldBe(VehicleParameterValue.Off);
+ }
+
+ [Fact]
+ public void SpawnData_get_should_succeed()
+ {
+ var data = _vehicle.SpawnData;
+ data.ModelId.ShouldBe((int)VehicleModelType.Landstalker);
+ }
+
+ [Fact]
+ public void NumberPlate_get_should_succeed()
+ {
+ _vehicle.SetNumberPlate("ABC123");
+ _vehicle.NumberPlate.ShouldBe("ABC123");
+ }
+
+ [Fact]
+ public void IsDead_should_be_false_initially()
+ {
+ _vehicle.IsDead.ShouldBeFalse();
+ }
+
+ [Fact]
+ public void IsOccupied_should_be_false_initially()
+ {
+ _vehicle.IsOccupied.ShouldBeFalse();
+ }
+
+ [Fact]
+ public void IsRespawning_should_be_true_initially()
+ {
+ _vehicle.IsRespawning.ShouldBeTrue();
+ }
+
+ [Fact]
+ public void IsTrailer_should_be_false_for_non_trailer()
+ {
+ _vehicle.IsTrailer.ShouldBeFalse();
+ }
+
+ [Fact]
+ public void RespawnDelay_should_roundtrip()
+ {
+ _vehicle.RespawnDelay = TimeSpan.FromSeconds(30);
+ _vehicle.RespawnDelay.ShouldBe(TimeSpan.FromSeconds(30));
+ }
+
+ [Fact]
+ public void Driver_should_be_null_when_unoccupied()
+ {
+ _vehicle.Driver.ShouldBeNull();
+ }
+
+ [Fact]
+ public void Cab_should_be_null_when_not_towed()
+ {
+ _vehicle.Cab.ShouldBeNull();
+ }
+
+ [Fact]
+ public void GetPassengers_should_succeed()
+ {
+ _ = _vehicle.GetPassengers();
+ }
+
+ [Fact]
+ public void StreamedForPlayers_should_succeed()
+ {
+ _ = _vehicle.StreamedForPlayers();
+ }
+
+ [Fact]
+ public void SetSiren_should_succeed()
+ {
+ _vehicle.SetSiren(true);
+ _vehicle.SetSiren(false);
+ }
+
+ [Fact]
+ public void LastDriverPoolID_should_succeed()
+ {
+ _ = _vehicle.LastDriverPoolID;
+ }
}
\ No newline at end of file
diff --git a/test/TestMode.Entities.ApiTests/WorldServiceTests.cs b/test/TestMode.Entities.ApiTests/WorldServiceTests.cs
index e1d8bd76..7beca81a 100644
--- a/test/TestMode.Entities.ApiTests/WorldServiceTests.cs
+++ b/test/TestMode.Entities.ApiTests/WorldServiceTests.cs
@@ -1,4 +1,5 @@
-using Microsoft.Extensions.DependencyInjection;
+using System.Numerics;
+using Microsoft.Extensions.DependencyInjection;
using SampSharp.Entities.SAMP;
using Shouldly;
using Xunit;
@@ -33,4 +34,88 @@ public void Gravity_above_50_should_throw()
{
Should.Throw(() => Sut.Gravity = 50.1f);
}
+
+ [Fact]
+ public void CreateStaticVehicle_should_succeed()
+ {
+ var vehicle = Sut.CreateStaticVehicle(VehicleModelType.BMX, new Vector3(1, 2, 3), 0, 0, 0);
+
+ try
+ {
+ vehicle.ShouldNotBeNull();
+ vehicle.Model.ShouldBe(VehicleModelType.BMX);
+ }
+ finally
+ {
+ vehicle.Destroy();
+ }
+ }
+
+ [Fact]
+ public void SetObjectsDefaultCameraCollision_should_succeed()
+ {
+ Sut.SetObjectsDefaultCameraCollision(true);
+ Sut.SetObjectsDefaultCameraCollision(false);
+ }
+
+ [Fact]
+ public void SendClientMessage_with_color_should_succeed()
+ {
+ Sut.SendClientMessage(new Color(255, 0, 0), "Test message");
+ }
+
+ [Fact]
+ public void SendClientMessage_with_color_and_format_should_succeed()
+ {
+ Sut.SendClientMessage(new Color(255, 0, 0), "Test {0}", "message");
+ }
+
+ [Fact]
+ public void SendClientMessage_without_color_should_succeed()
+ {
+ Sut.SendClientMessage("Test message");
+ }
+
+ [Fact]
+ public void SendClientMessage_without_color_with_format_should_succeed()
+ {
+ Sut.SendClientMessage("Test {0}", "message");
+ }
+
+ [Fact]
+ public void SendPlayerMessageToPlayer_should_succeed()
+ {
+ Sut.SendPlayerMessageToPlayer(Player, "Test message");
+ }
+
+ [Fact]
+ public void SendDeathMessage_should_succeed()
+ {
+ Sut.SendDeathMessage(Player, Player, Weapon.Colt45);
+ }
+
+ [Fact]
+ public void GameText_should_succeed()
+ {
+ Sut.GameText("Test", TimeSpan.FromSeconds(5), GameTextStyle.Style1);
+ }
+
+ [Fact]
+ public void HideGameText_should_succeed()
+ {
+ Sut.GameText("Test", TimeSpan.FromSeconds(5), GameTextStyle.Style1);
+ Sut.HideGameText(GameTextStyle.Style1);
+ }
+
+ [Fact]
+ public void CreateExplosion_should_succeed()
+ {
+ Sut.CreateExplosion(new Vector3(1, 2, 3), ExplosionType.LargeInvisible, 10.0f);
+ }
+
+ [Fact]
+ public void SetWeather_should_succeed()
+ {
+ Sut.SetWeather(1);
+ }
}
diff --git a/test/TestMode.OpenMp.Core/Startup.cs b/test/TestMode.OpenMp.Core/Startup.cs
index 34ad986c..fb99591e 100644
--- a/test/TestMode.OpenMp.Core/Startup.cs
+++ b/test/TestMode.OpenMp.Core/Startup.cs
@@ -1,4 +1,5 @@
using System.Numerics;
+using System.Runtime.InteropServices;
using SampSharp.OpenMp.Core;
using SampSharp.OpenMp.Core.Api;
using SampSharp.OpenMp.Core.RobinHood;
@@ -182,5 +183,10 @@ public void Initialize(IStartupContext context)
{
Console.WriteLine($"TD: {td.GetID()}, prev mdl: {td.GetPreviewModel()}");
}
+
+ // default color test
+ Console.WriteLine($"sizeof(Colour) = {Marshal.SizeOf()}");
+ var color = context.Core.GetPlayers().GetDefaultColour(15);
+ Console.WriteLine(color);
}
}
\ No newline at end of file