Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Read AGENTS.md in the repository root and follow the instructions it contains.
2 changes: 2 additions & 0 deletions SampSharp.slnx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@
<Project Path="src/SampSharp.SourceGenerator/SampSharp.SourceGenerator.csproj" />
</Folder>
<Folder Name="/test/">
<Project Path="test/SampSharp.Analyzer.Tests/SampSharp.Analyzer.Tests.csproj" />
<Project Path="test/SampSharp.CodeFixes.Tests/SampSharp.CodeFixes.Tests.csproj" />
<Project Path="test/SampSharp.OpenMp.Entities.Commands.Tests/SampSharp.OpenMp.Entities.Commands.Tests.csproj" />
<Project Path="test/SampSharp.OpenMp.Core.Tests/SampSharp.OpenMp.Core.Tests.csproj" />
<Project Path="test/SampSharp.OpenMp.Entities.Tests/SampSharp.OpenMp.Entities.Tests.csproj" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -84,8 +84,8 @@ private static async Task<Document> 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")));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,15 +65,15 @@ private static async Task<Document> Fix(Document document, StructDeclarationSynt
{
newStructDeclaration = newStructDeclaration
.WithModifiers(
structDeclaration.Modifiers.Add(
newStructDeclaration.Modifiers.Add(
Token(SyntaxKind.PartialKeyword)));
}

if (!newStructDeclaration.Modifiers.Any(SyntaxKind.ReadOnlyKeyword))
{
newStructDeclaration = newStructDeclaration
.WithModifiers(
structDeclaration.Modifiers.Insert(0,
newStructDeclaration.Modifiers.Insert(0,
Token(SyntaxKind.ReadOnlyKeyword)));
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ public readonly partial struct IPlayerClassData
/// Gets the player's current class information.
/// </summary>
/// <returns>A reference to the player's class data.</returns>
public partial ref PlayerClass GetClass();
public partial BlittableStructRef<PlayerClass> GetClass();

/// <summary>
/// Sets the spawn information for the player's class.
Expand Down
9 changes: 7 additions & 2 deletions src/SampSharp.OpenMp.Core/Api/Components/Vehicles/IVehicle.cs
Original file line number Diff line number Diff line change
Expand Up @@ -259,8 +259,13 @@ public VehicleParams GetParams()
/// Gets the respawn delay of the vehicle.
/// </summary>
/// <returns>The respawn delay of the vehicle.</returns>
[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);

/// <summary>
/// Sets the respawn delay of the vehicle.
Expand Down
9 changes: 8 additions & 1 deletion src/SampSharp.OpenMp.Core/Api/Player/IPlayerPool.cs
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -198,7 +199,13 @@ public readonly partial struct IPlayerPool
/// </summary>
/// <param name="pid">The player ID.</param>
/// <returns>The default colour.</returns>
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);

/// <summary>
/// Converts this instance to a read-only player pool.
Expand Down
13 changes: 12 additions & 1 deletion src/SampSharp.OpenMp.Core/Std/Pair.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,17 @@ public readonly struct Pair<T1, T2>
/// </summary>
public readonly T2 Second;

/// <summary>
/// Initializes a new instance of the <see cref="Pair{T1,T2}" /> struct.
/// </summary>
/// <param name="first">The first value in the pair.</param>
/// <param name="second">The second value in the pair.</param>
public Pair(T1 first, T2 second)
{
First = first;
Second = second;
}

/// <summary>
/// Deconstructs the pair into its two values.
/// </summary>
Expand Down Expand Up @@ -54,6 +65,6 @@ public static implicit operator (T1, T2)(Pair<T1, T2> pair)
/// <param name="tuple">The tuple to convert.</param>
public static implicit operator Pair<T1, T2>((T1 first,T2 second) tuple)
{
return (tuple.first, tuple.second);
return new Pair<T1, T2>(tuple.first, tuple.second);
}
}
2 changes: 1 addition & 1 deletion src/SampSharp.OpenMp.Entities/SAMP/Components/Player.cs
Original file line number Diff line number Diff line change
Expand Up @@ -812,7 +812,7 @@ public virtual void SetSpawnInfo(PlayerSpawnData spawnData)
/// <returns>A <see cref="PlayerSpawnData"/> instance containing the player's spawn position, orientation, and related data.</returns>
public virtual PlayerSpawnData GetSpawnInfo()
{
ref var data = ref ClassData.GetClass();
var data = ClassData.GetClass().Value;
return PlayerSpawnData.FromOmpData(ref data);
}

Expand Down
9 changes: 0 additions & 9 deletions src/SampSharp.OpenMp.Entities/SAMP/Components/PlayerPickup.cs
Original file line number Diff line number Diff line change
Expand Up @@ -43,13 +43,4 @@ public virtual void StreamOut()
{
_pickup.StreamOutForPlayer(_player);
}

/// <summary>
/// Gets or sets a value indicating whether the pickup is hidden for the player.
/// </summary>
public virtual bool IsHidden
{
get => _pickup.IsPickupHiddenForPlayer(_player);
set => _pickup.SetPickupHiddenForPlayer(_player, value);
}
}
6 changes: 6 additions & 0 deletions src/SampSharp.OpenMp.Entities/SAMP/Components/Vehicle.cs
Original file line number Diff line number Diff line change
Expand Up @@ -761,6 +761,12 @@ protected override void OnDestroyComponent()
/// <inheritdoc />
public override string ToString()
{
if (IsDestroying)
{
// TODO: do this check for other components as well
return "(Destroyed)";
}

return $"(Id: {Id}, Model: {Model})";
}

Expand Down
11 changes: 6 additions & 5 deletions src/sampsharp-component/proxies/api.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
#include <Server/Components/Vehicles/vehicles.hpp>

#include "../proxy-api.hpp"
#include <iostream>

#ifdef __clang__
#pragma clang diagnostic push
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -1032,7 +1033,7 @@ PROXY(IPlayerPool, void, broadcastRPC, int, Span<uint8_t>, 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<IPlayer>, IReadOnlyPool);

PROXY_EVENT_DISPATCHER(IPlayerPool, PlayerSpawnEventHandler, getPlayerSpawnDispatcher);
Expand Down
74 changes: 74 additions & 0 deletions test/SampSharp.Analyzer.Tests/AnalyzerTestHelper.cs
Original file line number Diff line number Diff line change
@@ -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;

/// <summary>
/// Lightweight helper that compiles a C# source string with the SampSharp.OpenMp.Core
/// references available and runs an analyzer against it.
/// </summary>
internal static class AnalyzerTestHelper
{
private static readonly Lazy<IReadOnlyList<MetadataReference>> _references = new(BuildReferences);

public static async Task<ImmutableArray<Diagnostic>> 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<MetadataReference> BuildReferences()
{
var runtimeDir = Path.GetDirectoryName(typeof(object).Assembly.Location)!;
var refs = new List<MetadataReference>();

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;
}
}
23 changes: 23 additions & 0 deletions test/SampSharp.Analyzer.Tests/SampSharp.Analyzer.Tests.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<OutputType>Exe</OutputType>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" />
<PackageReference Include="Microsoft.NET.Test.Sdk" />
<PackageReference Include="Shouldly" />
<PackageReference Include="xunit.v3" />
<PackageReference Include="xunit.runner.visualstudio">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\..\src\SampSharp.OpenMp.Core\SampSharp.OpenMp.Core.csproj" />
<ProjectReference Include="..\..\src\SampSharp.Analyzer\SampSharp.Analyzer.csproj" />
</ItemGroup>

</Project>
Original file line number Diff line number Diff line change
@@ -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);
}
}
Loading
Loading