diff --git a/.editorconfig b/.editorconfig
index 547bde6fc9..f9a731d641 100644
--- a/.editorconfig
+++ b/.editorconfig
@@ -2,7 +2,6 @@ root = true
# See the documentation for reference:
# https://docs.microsoft.com/en-us/visualstudio/ide/editorconfig-code-style-settings-reference
-
[*]
indent_style = space
@@ -34,7 +33,7 @@ csharp_new_line_between_query_expression_clauses = true
# Indentation
csharp_indent_case_contents = true
csharp_indent_switch_labels = true
-csharp_indent_labels = false
+csharp_indent_labels = flush_left
# Spacing
csharp_space_after_cast = false
@@ -67,13 +66,13 @@ dotnet_style_collection_initializer = true:warning
dotnet_style_coalesce_expression = true:suggestion # Prefer a ?? b
dotnet_style_null_propagation = false:warning # Do not prefer foo?.bar
-# Implicit and explicity styles (disallow use of `var` - types have to be specified explicitly)
+# Implicit and explicit styles (disallow use of `var` - types have to be specified explicitly)
csharp_style_var_for_built_in_types = false:warning
csharp_style_var_when_type_is_apparent = false:warning
csharp_style_var_elsewhere = false:warning
# Expression-bodied members
-csharp_style_expression_bodied_methods = false:none
+csharp_style_expression_bodied_methods = when_on_single_line:none
# Null checking preferences
csharp_style_conditional_delegate_call = false:warning # Do not prefer foo?.Invoke(bar); over if (foo != null) { foo(bar); }, with all it's newlines.
diff --git a/Directory.Build.props b/Directory.Build.props
index 859b2e9a97..6d586b0ba6 100644
--- a/Directory.Build.props
+++ b/Directory.Build.props
@@ -3,6 +3,7 @@
1.6.0.1
10
+ false
false
false
false
@@ -18,6 +19,9 @@
+ true
+
+
true
@@ -27,6 +31,26 @@
true
+
+
+
+
+
+ JB
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Nitrox.Analyzers/Diagnostics/LocalizationAnalyzer.cs b/Nitrox.Analyzers/Diagnostics/LocalizationAnalyzer.cs
new file mode 100644
index 0000000000..4a49d9f4be
--- /dev/null
+++ b/Nitrox.Analyzers/Diagnostics/LocalizationAnalyzer.cs
@@ -0,0 +1,166 @@
+using System;
+using System.Collections.Generic;
+using System.Collections.Immutable;
+using System.IO;
+using System.Text.RegularExpressions;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp;
+using Microsoft.CodeAnalysis.CSharp.Syntax;
+using Microsoft.CodeAnalysis.Diagnostics;
+
+namespace Nitrox.Analyzers.Diagnostics;
+
+///
+/// Tests that requested localization keys exist in the English localization file.
+///
+[DiagnosticAnalyzer(LanguageNames.CSharp)]
+public sealed class LocalizationAnalyzer : DiagnosticAnalyzer
+{
+ public const string INVALID_LOCALIZATION_KEY_DIAGNOSTIC_ID = $"{nameof(LocalizationAnalyzer)}001";
+
+ private const string NITROX_LOCALIZATION_PREFIX = "Nitrox_";
+ private static readonly string relativePathFromSolutionDirToEnglishLanguageFile = Path.Combine("Nitrox.Assets.Subnautica", "LanguageFiles", "en.json");
+ private static readonly Regex localizationParseRegex = new(@"^\s*""([^""]+)""\s*:\s*""([^""]+)""", RegexOptions.Compiled | RegexOptions.Multiline);
+
+ private static readonly DiagnosticDescriptor invalidLocalizationKeyRule = new(INVALID_LOCALIZATION_KEY_DIAGNOSTIC_ID,
+ "Tests localization usages are valid",
+ "Localization key '{0}' does not exist in '{1}'",
+ "Usage",
+ DiagnosticSeverity.Warning,
+ true,
+ "Tests that requested localization keys exist in the English localization file");
+
+ ///
+ /// Gets the list of rules of supported diagnostics.
+ ///
+ public override ImmutableArray SupportedDiagnostics => ImmutableArray.Create(invalidLocalizationKeyRule);
+
+ ///
+ /// Initializes the analyzer by registering on symbol occurrence in the targeted code.
+ ///
+ public override void Initialize(AnalysisContext context)
+ {
+ context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
+ context.EnableConcurrentExecution();
+
+ context.RegisterCompilationStartAction(startContext =>
+ {
+ startContext.Options.AnalyzerConfigOptionsProvider.GlobalOptions.TryGetValue("build_property.projectdir", out string projectDir);
+ if (LocalizationHelper.Load(projectDir))
+ {
+ startContext.RegisterSyntaxNodeAction(AnalyzeStringNode, SyntaxKind.StringLiteralExpression);
+ }
+ });
+ }
+
+ ///
+ /// Analyzes string literals in code that are passed as argument to 'Language.main.Get'.
+ ///
+ private void AnalyzeStringNode(SyntaxNodeAnalysisContext context)
+ {
+ LiteralExpressionSyntax expression = (LiteralExpressionSyntax)context.Node;
+ if (expression.Parent is not ArgumentSyntax argument)
+ {
+ return;
+ }
+ if (argument.Parent is not { Parent: InvocationExpressionSyntax invocation })
+ {
+ return;
+ }
+ if (context.SemanticModel.GetSymbolInfo(invocation).Symbol is not IMethodSymbol method)
+ {
+ return;
+ }
+ if (method is not { ContainingType.Name: "Language", Name: "Get" })
+ {
+ return;
+ }
+ // Ignore language call for non-nitrox keys.
+ string stringValue = expression.Token.ValueText;
+ if (!stringValue.StartsWith(NITROX_LOCALIZATION_PREFIX, StringComparison.OrdinalIgnoreCase))
+ {
+ return;
+ }
+ if (LocalizationHelper.ContainsKey(stringValue))
+ {
+ return;
+ }
+ context.ReportDiagnostic(Diagnostic.Create(invalidLocalizationKeyRule, context.Node.GetLocation(), stringValue, LocalizationHelper.FileName));
+ }
+
+ ///
+ /// Wrapper API for synchronized access to the English localization file.
+ ///
+ private static class LocalizationHelper
+ {
+ private static readonly object locker = new();
+ private static string EnglishLocalizationFileName { get; set; } = "";
+ private static ImmutableDictionary EnglishLocalization { get; set; } = ImmutableDictionary.Empty;
+
+ public static bool IsEmpty
+ {
+ get
+ {
+ lock (locker)
+ {
+ return EnglishLocalization.IsEmpty;
+ }
+ }
+ }
+
+ public static string FileName
+ {
+ get
+ {
+ lock (locker)
+ {
+ return EnglishLocalizationFileName;
+ }
+ }
+ }
+
+ public static bool ContainsKey(string key)
+ {
+ lock (locker)
+ {
+ return EnglishLocalization.ContainsKey(key);
+ }
+ }
+
+ public static bool Load(string projectDir)
+ {
+ if (string.IsNullOrWhiteSpace(projectDir))
+ {
+ return false;
+ }
+ string solutionDir = Directory.GetParent(projectDir)?.Parent?.FullName;
+ if (!Directory.Exists(solutionDir))
+ {
+ return false;
+ }
+
+ string enJson;
+ lock (locker)
+ {
+ EnglishLocalizationFileName = Path.Combine(solutionDir, relativePathFromSolutionDirToEnglishLanguageFile);
+ if (!File.Exists(EnglishLocalizationFileName))
+ {
+ return false;
+ }
+
+ enJson = File.ReadAllText(EnglishLocalizationFileName);
+ }
+ // Parse localization JSON to dictionary for lookup.
+ Dictionary keyValue = new();
+ foreach (Match match in localizationParseRegex.Matches(enJson))
+ {
+ keyValue.Add(match.Groups[1].Value, match.Groups[2].Value);
+ }
+ lock (locker)
+ {
+ EnglishLocalization = keyValue.ToImmutableDictionary();
+ }
+ return true;
+ }
+ }
+}
diff --git a/Nitrox.Analyzers/Diagnostics/StringUsageAnalyzer.cs b/Nitrox.Analyzers/Diagnostics/StringUsageAnalyzer.cs
new file mode 100644
index 0000000000..33cd553a7b
--- /dev/null
+++ b/Nitrox.Analyzers/Diagnostics/StringUsageAnalyzer.cs
@@ -0,0 +1,80 @@
+using System.Collections.Immutable;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp;
+using Microsoft.CodeAnalysis.CSharp.Syntax;
+using Microsoft.CodeAnalysis.Diagnostics;
+
+namespace Nitrox.Analyzers.Diagnostics;
+
+[DiagnosticAnalyzer(LanguageNames.CSharp)]
+public sealed class StringUsageAnalyzer : DiagnosticAnalyzer
+{
+ public const string PREFER_INTERPOLATED_STRING_DIAGNOSTIC_ID = $"{nameof(StringUsageAnalyzer)}001";
+
+ private static readonly DiagnosticDescriptor preferInterpolatedStringRule = new(PREFER_INTERPOLATED_STRING_DIAGNOSTIC_ID,
+ "Prefer interpolated string over string concat",
+ "String concat can be turned into interpolated string",
+ "Usage",
+ DiagnosticSeverity.Warning,
+ true,
+ "Prefer interpolated string over concatenating strings");
+
+ public override ImmutableArray SupportedDiagnostics => ImmutableArray.Create(preferInterpolatedStringRule);
+
+ public override void Initialize(AnalysisContext context)
+ {
+ context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
+ context.EnableConcurrentExecution();
+
+ context.RegisterSyntaxNodeAction(AnalyzeAddNode, SyntaxKind.AddExpression);
+ }
+
+ private void AnalyzeAddNode(SyntaxNodeAnalysisContext context)
+ {
+ bool IsPartOfStringConcat(SyntaxNode node)
+ {
+ switch (node)
+ {
+ case LiteralExpressionSyntax literal:
+ return literal.IsKind(SyntaxKind.StringLiteralExpression);
+ case InterpolatedStringExpressionSyntax:
+ return true;
+ case MemberAccessExpressionSyntax member:
+ string memberType = context.SemanticModel.GetTypeInfo(member).ConvertedType?.Name;
+ return memberType == "String";
+ case BinaryExpressionSyntax binary:
+ // If one side is string-ish then the other side will get implicitly casted to string.
+ return binary.IsKind(SyntaxKind.AddExpression) && (IsPartOfStringConcat(binary.Right) || IsPartOfStringConcat(binary.Left));
+ default:
+ return false;
+ }
+ }
+
+ static bool IsLeftMostNodeInConcat(SyntaxNode node)
+ {
+ switch (node)
+ {
+ case BinaryExpressionSyntax:
+ case InterpolatedStringContentSyntax:
+ return true;
+ case ParenthesizedExpressionSyntax:
+ return IsLeftMostNodeInConcat(node.Parent);
+ }
+ return false;
+ }
+
+ BinaryExpressionSyntax expression = (BinaryExpressionSyntax)context.Node;
+ // Deduplicate warnings. Only left most '+' of the expression should be handled here.
+ if (IsLeftMostNodeInConcat(expression.Parent))
+ {
+ return;
+ }
+ // Test if this should be interpolated.
+ if (!IsPartOfStringConcat(expression.Left) && !IsPartOfStringConcat(expression.Right))
+ {
+ return;
+ }
+
+ context.ReportDiagnostic(Diagnostic.Create(preferInterpolatedStringRule, expression.GetLocation(), expression));
+ }
+}
diff --git a/Nitrox.Analyzers/Diagnostics/UnitySkippedObjectLifetimeAnalyzer.cs b/Nitrox.Analyzers/Diagnostics/UnitySkippedObjectLifetimeAnalyzer.cs
new file mode 100644
index 0000000000..26a6df3c29
--- /dev/null
+++ b/Nitrox.Analyzers/Diagnostics/UnitySkippedObjectLifetimeAnalyzer.cs
@@ -0,0 +1,123 @@
+using System.Collections.Immutable;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp;
+using Microsoft.CodeAnalysis.CSharp.Syntax;
+using Microsoft.CodeAnalysis.Diagnostics;
+using Nitrox.Analyzers.Extensions;
+
+namespace Nitrox.Analyzers.Diagnostics;
+
+///
+/// Test that Unity objects are properly checked for their lifetime.
+/// The lifetime check is skipped when using "is null" or "obj?.member" as opposed to "== null".
+///
+[DiagnosticAnalyzer(LanguageNames.CSharp)]
+public sealed class UnitySkippedObjectLifetimeAnalyzer : DiagnosticAnalyzer
+{
+ public const string FIX_FUNCTION_NAME = "AliveOrNull";
+ public const string FIX_FUNCTION_NAMESPACE = "NitroxClient.Unity.Helper";
+ public const string CONDITIONAL_ACCESS_DIAGNOSTIC_ID = $"{nameof(UnitySkippedObjectLifetimeAnalyzer)}001";
+ public const string IS_NULL_DIAGNOSTIC_ID = $"{nameof(UnitySkippedObjectLifetimeAnalyzer)}002";
+ public const string NULL_COALESCE_DIAGNOSTIC_ID = $"{nameof(UnitySkippedObjectLifetimeAnalyzer)}003";
+ private const string RULE_TITLE = "Tests that Unity object lifetime is not ignored";
+ private const string RULE_DESCRIPTION = "Tests that Unity object lifetime checks are not ignored.";
+
+ private static readonly DiagnosticDescriptor conditionalAccessRule = new(CONDITIONAL_ACCESS_DIAGNOSTIC_ID,
+ RULE_TITLE,
+ "'?.' is invalid on type '{0}' as it derives from 'UnityEngine.Object', bypassing the Unity object lifetime check",
+ "Usage",
+ DiagnosticSeverity.Error,
+ true,
+ RULE_DESCRIPTION);
+
+ private static readonly DiagnosticDescriptor isNullRule = new(IS_NULL_DIAGNOSTIC_ID,
+ RULE_TITLE,
+ "'is null' is invalid on type '{0}' as it derives from 'UnityEngine.Object', bypassing the Unity object lifetime check",
+ "Usage",
+ DiagnosticSeverity.Error,
+ true,
+ RULE_DESCRIPTION);
+
+ private static readonly DiagnosticDescriptor nullCoalesceRule = new(NULL_COALESCE_DIAGNOSTIC_ID,
+ RULE_TITLE,
+ "'??' is invalid on type '{0}' as it derives from 'UnityEngine.Object', bypassing the Unity object lifetime check",
+ "Usage",
+ DiagnosticSeverity.Error,
+ true,
+ RULE_DESCRIPTION);
+
+ ///
+ /// Gets the list of rules of supported diagnostics.
+ ///
+ public override ImmutableArray SupportedDiagnostics => ImmutableArray.Create(conditionalAccessRule, isNullRule, nullCoalesceRule);
+
+ ///
+ /// Initializes the analyzer by registering on symbol occurrence in the targeted code.
+ ///
+ public override void Initialize(AnalysisContext context)
+ {
+ context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
+ context.EnableConcurrentExecution();
+
+ context.RegisterCompilationStartAction(compStartContext =>
+ {
+ INamedTypeSymbol unityObjectTypeSymbol = compStartContext.Compilation.GetTypeByMetadataName("UnityEngine.Object");
+ if (unityObjectTypeSymbol == null)
+ {
+ return;
+ }
+
+ compStartContext.RegisterSyntaxNodeAction(c => AnalyzeIsNullNode(c, unityObjectTypeSymbol), SyntaxKind.IsPatternExpression);
+ compStartContext.RegisterSyntaxNodeAction(c => AnalyzeConditionalAccessNode(c, unityObjectTypeSymbol), SyntaxKind.ConditionalAccessExpression);
+ compStartContext.RegisterSyntaxNodeAction(c => AnalyzeCoalesceNode(c, unityObjectTypeSymbol), SyntaxKind.CoalesceExpression);
+ });
+ }
+
+ private void AnalyzeIsNullNode(SyntaxNodeAnalysisContext context, ITypeSymbol unityObjectSymbol)
+ {
+ IsPatternExpressionSyntax expression = (IsPatternExpressionSyntax)context.Node;
+ // Is this a "is null" check?
+ if (expression.Pattern is not ConstantPatternSyntax constantPattern)
+ {
+ return;
+ }
+ if (constantPattern.Expression is not LiteralExpressionSyntax literal || !literal.Token.IsKind(SyntaxKind.NullKeyword))
+ {
+ return;
+ }
+ // Is it on a UnityEngine.Object?
+ if (IsUnityObjectExpression(context, expression.Expression, unityObjectSymbol, out ITypeSymbol originSymbol))
+ {
+ context.ReportDiagnostic(Diagnostic.Create(isNullRule, constantPattern.GetLocation(), originSymbol!.Name));
+ }
+ }
+
+ private void AnalyzeConditionalAccessNode(SyntaxNodeAnalysisContext context, ITypeSymbol unityObjectSymbol)
+ {
+ static bool IsFixedWithAliveOrNull(SyntaxNodeAnalysisContext context, ConditionalAccessExpressionSyntax expression)
+ {
+ return (context.SemanticModel.GetSymbolInfo(expression.Expression).Symbol as IMethodSymbol)?.Name == FIX_FUNCTION_NAME;
+ }
+
+ ConditionalAccessExpressionSyntax expression = (ConditionalAccessExpressionSyntax)context.Node;
+ if (IsUnityObjectExpression(context, expression.Expression, unityObjectSymbol, out ITypeSymbol originSymbol) && !IsFixedWithAliveOrNull(context, expression))
+ {
+ context.ReportDiagnostic(Diagnostic.Create(conditionalAccessRule, context.Node.GetLocation(), originSymbol!.Name));
+ }
+ }
+
+ private void AnalyzeCoalesceNode(SyntaxNodeAnalysisContext context, ITypeSymbol unityObjectSymbol)
+ {
+ BinaryExpressionSyntax expression = (BinaryExpressionSyntax)context.Node;
+ if (IsUnityObjectExpression(context, expression.Left, unityObjectSymbol, out ITypeSymbol originSymbol))
+ {
+ context.ReportDiagnostic(Diagnostic.Create(nullCoalesceRule, context.Node.GetLocation(), originSymbol!.Name));
+ }
+ }
+
+ private bool IsUnityObjectExpression(SyntaxNodeAnalysisContext context, ExpressionSyntax possibleUnityAccessExpression, ITypeSymbol compareSymbol, out ITypeSymbol possibleUnitySymbol)
+ {
+ possibleUnitySymbol = context.SemanticModel.GetTypeInfo(possibleUnityAccessExpression).Type;
+ return possibleUnitySymbol.IsType(compareSymbol);
+ }
+}
diff --git a/Nitrox.Analyzers/Extensions/DebugExtensions.cs b/Nitrox.Analyzers/Extensions/DebugExtensions.cs
new file mode 100644
index 0000000000..e1179f50be
--- /dev/null
+++ b/Nitrox.Analyzers/Extensions/DebugExtensions.cs
@@ -0,0 +1,38 @@
+using System;
+using System.Collections.Concurrent;
+using System.Diagnostics;
+using System.IO;
+using System.Threading.Tasks;
+
+namespace Nitrox.Analyzers.Extensions;
+
+public static class DebugExtensions
+{
+ private static readonly string desktopPath = Environment.GetFolderPath(Environment.SpecialFolder.Desktop);
+ private static readonly ConcurrentQueue<(object source, string message)> logQueue = new();
+ private static readonly object logLocker = new();
+
+ ///
+ /// Can be used to test analyzers.
+ ///
+ [Conditional("DEBUG")]
+ public static void Log(this object analyzer, string message)
+ {
+ logQueue.Enqueue((analyzer, message));
+ Task.Run(() =>
+ {
+ while (!logQueue.IsEmpty)
+ {
+ if (!logQueue.TryDequeue(out (object source, string message) pair))
+ {
+ continue;
+ }
+
+ lock (logLocker)
+ {
+ File.AppendAllText(Path.Combine(desktopPath, $"{pair.source.GetType().Name}.log"), pair.message + Environment.NewLine);
+ }
+ }
+ });
+ }
+}
diff --git a/Nitrox.Analyzers/Extensions/SymbolExtensions.cs b/Nitrox.Analyzers/Extensions/SymbolExtensions.cs
new file mode 100644
index 0000000000..7ac60aee2e
--- /dev/null
+++ b/Nitrox.Analyzers/Extensions/SymbolExtensions.cs
@@ -0,0 +1,28 @@
+using Microsoft.CodeAnalysis;
+
+namespace Nitrox.Analyzers.Extensions;
+
+public static class SymbolExtensions
+{
+ public static bool IsType(this ITypeSymbol symbol, SemanticModel semanticModel, string fullyQualifiedTypeName)
+ {
+ return symbol.IsType(semanticModel.Compilation.GetTypeByMetadataName(fullyQualifiedTypeName));
+ }
+
+ public static bool IsType(this ITypeSymbol symbol, ITypeSymbol targetingSymbol)
+ {
+ if (SymbolEqualityComparer.Default.Equals(symbol, targetingSymbol))
+ {
+ return true;
+ }
+ while (symbol.BaseType is { } baseTypeSymbol)
+ {
+ symbol = baseTypeSymbol;
+ if (SymbolEqualityComparer.Default.Equals(symbol, targetingSymbol))
+ {
+ return true;
+ }
+ }
+ return false;
+ }
+}
diff --git a/Nitrox.Analyzers/Fixers/UnitySkippedObjectLifetimeFixProvider.cs b/Nitrox.Analyzers/Fixers/UnitySkippedObjectLifetimeFixProvider.cs
new file mode 100644
index 0000000000..b86e600d20
--- /dev/null
+++ b/Nitrox.Analyzers/Fixers/UnitySkippedObjectLifetimeFixProvider.cs
@@ -0,0 +1,70 @@
+extern alias JB;
+using System.Collections.Immutable;
+using System.Composition;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using JB::JetBrains.Annotations;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CodeActions;
+using Microsoft.CodeAnalysis.CodeFixes;
+using Microsoft.CodeAnalysis.CSharp;
+using Microsoft.CodeAnalysis.CSharp.Syntax;
+using Microsoft.CodeAnalysis.Text;
+using Nitrox.Analyzers.Diagnostics;
+using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory;
+
+namespace Nitrox.Analyzers.Fixers;
+
+[Shared]
+[UsedImplicitly]
+[ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(UnitySkippedObjectLifetimeFixProvider))]
+public sealed class UnitySkippedObjectLifetimeFixProvider : CodeFixProvider
+{
+ private static readonly IdentifierNameSyntax aliveOrNull = IdentifierName(UnitySkippedObjectLifetimeAnalyzer.FIX_FUNCTION_NAME);
+ public override ImmutableArray FixableDiagnosticIds => ImmutableArray.Create(UnitySkippedObjectLifetimeAnalyzer.CONDITIONAL_ACCESS_DIAGNOSTIC_ID);
+
+ public override FixAllProvider GetFixAllProvider() => WellKnownFixAllProviders.BatchFixer;
+
+ public override async Task RegisterCodeFixesAsync(CodeFixContext context)
+ {
+ // Code template from: https://learn.microsoft.com/en-us/dotnet/csharp/roslyn-sdk/tutorials/how-to-write-csharp-analyzer-code-fix
+ SyntaxNode root = await context.Document.GetSyntaxRootAsync(context.CancellationToken)
+ .ConfigureAwait(false);
+ Diagnostic diagnostic = context.Diagnostics.First();
+ TextSpan diagnosticSpan = diagnostic.Location.SourceSpan;
+ ConditionalAccessExpressionSyntax declaration = root!.FindToken(diagnosticSpan.Start).Parent!.AncestorsAndSelf()
+ .OfType()
+ .First();
+ context.RegisterCodeFix(
+ CodeAction.Create(
+ equivalenceKey: UnitySkippedObjectLifetimeAnalyzer.CONDITIONAL_ACCESS_DIAGNOSTIC_ID,
+ title: "Insert AliveOrNull() before conditional access of UnityEngine.Object",
+ createChangedDocument: c => InsertAliveOrNullAsync(context.Document, declaration, c)
+ ),
+ diagnostic);
+ }
+
+ private async Task InsertAliveOrNullAsync(Document document, ConditionalAccessExpressionSyntax declaration, CancellationToken cancellationToken)
+ {
+ SyntaxNode root = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false);
+ if (root == null)
+ {
+ return document;
+ }
+
+ // 1. Wrap expression with an invocation to AliveOrNull, this will cause AliveOrNull to be called before the conditional access.
+ InvocationExpressionSyntax wrappedExpression = InvocationExpression(MemberAccessExpression(SyntaxKind.SimpleMemberAccessExpression, declaration.Expression, aliveOrNull));
+ SyntaxNode newDeclaration = declaration.ReplaceNode(declaration.Expression, wrappedExpression);
+ root = root!.ReplaceNode(declaration, newDeclaration);
+ // 2. Ensure using statement for extension method .AliveOrNull().
+ // This is done after the "AliveOrNull" wrap because the declaration instance can't be found when root instance updates.
+ if (root is CompilationUnitSyntax compilationRoot && compilationRoot.Usings.All(u => u.Name.ToString() != UnitySkippedObjectLifetimeAnalyzer.FIX_FUNCTION_NAMESPACE))
+ {
+ root = compilationRoot.AddUsings(UsingDirective(aliveOrNull));
+ }
+
+ // Replace the old document with the new.
+ return document.WithSyntaxRoot(root);
+ }
+}
diff --git a/Nitrox.Analyzers/Nitrox.Analyzers.csproj b/Nitrox.Analyzers/Nitrox.Analyzers.csproj
new file mode 100644
index 0000000000..c2538f3c2e
--- /dev/null
+++ b/Nitrox.Analyzers/Nitrox.Analyzers.csproj
@@ -0,0 +1,19 @@
+
+
+
+
+ netstandard2.0
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Nitrox.sln b/Nitrox.sln
index e69b1d74c6..afe01605f9 100644
--- a/Nitrox.sln
+++ b/Nitrox.sln
@@ -30,6 +30,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Nitrox.Test", "Nitrox.Test\
EndProject
Project("{D954291E-2A0B-460D-934E-DC6B0785DB48}") = "Nitrox.Assets.Subnautica", "Nitrox.Assets.Subnautica\Nitrox.Assets.Subnautica.shproj", "{79E92B6D-5D25-4254-AC9F-FA9A1CD3CBC6}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Nitrox.Analyzers", "Nitrox.Analyzers\Nitrox.Analyzers.csproj", "{EB99BD64-EF43-4B06-BBFB-EB5DFA96E55D}"
+EndProject
Global
GlobalSection(SharedMSBuildProjectFiles) = preSolution
Nitrox.Assets.Subnautica\Nitrox.Assets.Subnautica.projitems*{79e92b6d-5d25-4254-ac9f-fa9a1cd3cbc6}*SharedItemsImports = 13
@@ -75,6 +77,10 @@ Global
{E4D8C360-34E4-4BE6-909F-3791DD9169B5}.Debug|Any CPU.Build.0 = Debug|Any CPU
{E4D8C360-34E4-4BE6-909F-3791DD9169B5}.Release|Any CPU.ActiveCfg = Release|Any CPU
{E4D8C360-34E4-4BE6-909F-3791DD9169B5}.Release|Any CPU.Build.0 = Release|Any CPU
+ {EB99BD64-EF43-4B06-BBFB-EB5DFA96E55D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {EB99BD64-EF43-4B06-BBFB-EB5DFA96E55D}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {EB99BD64-EF43-4B06-BBFB-EB5DFA96E55D}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {EB99BD64-EF43-4B06-BBFB-EB5DFA96E55D}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
diff --git a/Nitrox.sln.DotSettings b/Nitrox.sln.DotSettings
index 65cc46f098..928ff98a1f 100644
--- a/Nitrox.sln.DotSettings
+++ b/Nitrox.sln.DotSettings
@@ -1,12 +1,64 @@
True
True
- <?xml version="1.0" encoding="utf-16"?><Profile name="Copy of Built-in: Full Cleanup"><CSReorderTypeMembers>True</CSReorderTypeMembers><AspOptimizeRegisterDirectives>True</AspOptimizeRegisterDirectives><HtmlReformatCode>True</HtmlReformatCode><JsInsertSemicolon>True</JsInsertSemicolon><FormatAttributeQuoteDescriptor>True</FormatAttributeQuoteDescriptor><CorrectVariableKindsDescriptor>True</CorrectVariableKindsDescriptor><VariablesToInnerScopesDescriptor>True</VariablesToInnerScopesDescriptor><StringToTemplatesDescriptor>True</StringToTemplatesDescriptor><JsReformatCode>True</JsReformatCode><JsFormatDocComments>True</JsFormatDocComments><RemoveRedundantQualifiersTs>True</RemoveRedundantQualifiersTs><OptimizeImportsTs>True</OptimizeImportsTs><OptimizeReferenceCommentsTs>True</OptimizeReferenceCommentsTs><PublicModifierStyleTs>True</PublicModifierStyleTs><ExplicitAnyTs>True</ExplicitAnyTs><TypeAnnotationStyleTs>True</TypeAnnotationStyleTs><RelativePathStyleTs>True</RelativePathStyleTs><AsInsteadOfCastTs>True</AsInsteadOfCastTs><XMLReformatCode>True</XMLReformatCode><CSCodeStyleAttributes ArrangeTypeAccessModifier="True" ArrangeTypeMemberAccessModifier="True" SortModifiers="True" RemoveRedundantParentheses="True" AddMissingParentheses="True" ArrangeBraces="False" ArrangeAttributes="True" ArrangeArgumentsStyle="True" ArrangeCodeBodyStyle="False" ArrangeVarStyle="False" /><RemoveCodeRedundanciesVB>True</RemoveCodeRedundanciesVB><CssAlphabetizeProperties>True</CssAlphabetizeProperties><VBOptimizeImports>True</VBOptimizeImports><VBShortenReferences>True</VBShortenReferences><RemoveCodeRedundancies>True</RemoveCodeRedundancies><CSUseAutoProperty>True</CSUseAutoProperty><CSMakeFieldReadonly>True</CSMakeFieldReadonly><CSMakeAutoPropertyGetOnly>True</CSMakeAutoPropertyGetOnly><CSArrangeQualifiers>True</CSArrangeQualifiers><CSFixBuiltinTypeReferences>True</CSFixBuiltinTypeReferences><CssReformatCode>True</CssReformatCode><VBReformatCode>True</VBReformatCode><VBFormatDocComments>True</VBFormatDocComments><CSOptimizeUsings><OptimizeUsings>True</OptimizeUsings><EmbraceInRegion>False</EmbraceInRegion><RegionName></RegionName></CSOptimizeUsings><CSShortenReferences>True</CSShortenReferences><CSReformatCode>True</CSReformatCode><CSharpFormatDocComments>True</CSharpFormatDocComments><IDEA_SETTINGS><profile version="1.0">
- <option name="myName" value="Copy of Built-in: Full Cleanup" />
-</profile></IDEA_SETTINGS></Profile>
- <?xml version="1.0" encoding="utf-16"?><Profile name="Nitrox"><CSReorderTypeMembers>True</CSReorderTypeMembers><AspOptimizeRegisterDirectives>True</AspOptimizeRegisterDirectives><HtmlReformatCode>True</HtmlReformatCode><JsInsertSemicolon>True</JsInsertSemicolon><FormatAttributeQuoteDescriptor>True</FormatAttributeQuoteDescriptor><CorrectVariableKindsDescriptor>True</CorrectVariableKindsDescriptor><VariablesToInnerScopesDescriptor>True</VariablesToInnerScopesDescriptor><StringToTemplatesDescriptor>True</StringToTemplatesDescriptor><JsReformatCode>True</JsReformatCode><JsFormatDocComments>True</JsFormatDocComments><RemoveRedundantQualifiersTs>True</RemoveRedundantQualifiersTs><OptimizeImportsTs>True</OptimizeImportsTs><OptimizeReferenceCommentsTs>True</OptimizeReferenceCommentsTs><PublicModifierStyleTs>True</PublicModifierStyleTs><ExplicitAnyTs>True</ExplicitAnyTs><TypeAnnotationStyleTs>True</TypeAnnotationStyleTs><RelativePathStyleTs>True</RelativePathStyleTs><AsInsteadOfCastTs>True</AsInsteadOfCastTs><XMLReformatCode>True</XMLReformatCode><CSCodeStyleAttributes ArrangeTypeAccessModifier="True" ArrangeTypeMemberAccessModifier="True" SortModifiers="True" RemoveRedundantParentheses="True" AddMissingParentheses="True" ArrangeBraces="False" ArrangeAttributes="True" ArrangeArgumentsStyle="True" ArrangeCodeBodyStyle="False" ArrangeVarStyle="False" /><RemoveCodeRedundanciesVB>True</RemoveCodeRedundanciesVB><CssAlphabetizeProperties>True</CssAlphabetizeProperties><VBOptimizeImports>True</VBOptimizeImports><VBShortenReferences>True</VBShortenReferences><RemoveCodeRedundancies>True</RemoveCodeRedundancies><CSUseAutoProperty>True</CSUseAutoProperty><CSMakeFieldReadonly>True</CSMakeFieldReadonly><CSMakeAutoPropertyGetOnly>True</CSMakeAutoPropertyGetOnly><CSArrangeQualifiers>True</CSArrangeQualifiers><CSFixBuiltinTypeReferences>True</CSFixBuiltinTypeReferences><CssReformatCode>True</CssReformatCode><VBReformatCode>True</VBReformatCode><VBFormatDocComments>True</VBFormatDocComments><CSOptimizeUsings><OptimizeUsings>True</OptimizeUsings><EmbraceInRegion>False</EmbraceInRegion><RegionName></RegionName></CSOptimizeUsings><CSReformatCode>True</CSReformatCode><CSharpFormatDocComments>True</CSharpFormatDocComments><IDEA_SETTINGS><profile version="1.0">
+ DO_NOT_SHOW
+
+ <?xml version="1.0" encoding="utf-16"?><Profile name="Nitrox"><CSReorderTypeMembers>True</CSReorderTypeMembers><AspOptimizeRegisterDirectives>True</AspOptimizeRegisterDirectives><HtmlReformatCode>True</HtmlReformatCode><JsInsertSemicolon>True</JsInsertSemicolon><FormatAttributeQuoteDescriptor>True</FormatAttributeQuoteDescriptor><CorrectVariableKindsDescriptor>True</CorrectVariableKindsDescriptor><VariablesToInnerScopesDescriptor>True</VariablesToInnerScopesDescriptor><StringToTemplatesDescriptor>True</StringToTemplatesDescriptor><JsReformatCode>True</JsReformatCode><JsFormatDocComments>True</JsFormatDocComments><RemoveRedundantQualifiersTs>True</RemoveRedundantQualifiersTs><OptimizeImportsTs>True</OptimizeImportsTs><OptimizeReferenceCommentsTs>True</OptimizeReferenceCommentsTs><PublicModifierStyleTs>True</PublicModifierStyleTs><ExplicitAnyTs>True</ExplicitAnyTs><TypeAnnotationStyleTs>True</TypeAnnotationStyleTs><RelativePathStyleTs>True</RelativePathStyleTs><AsInsteadOfCastTs>True</AsInsteadOfCastTs><XMLReformatCode>True</XMLReformatCode><CSCodeStyleAttributes ArrangeTypeAccessModifier="True" ArrangeTypeMemberAccessModifier="True" SortModifiers="True" RemoveRedundantParentheses="True" AddMissingParentheses="True" ArrangeAttributes="True" ArrangeArgumentsStyle="True" /><RemoveCodeRedundanciesVB>True</RemoveCodeRedundanciesVB><CssAlphabetizeProperties>True</CssAlphabetizeProperties><VBOptimizeImports>True</VBOptimizeImports><VBShortenReferences>True</VBShortenReferences><RemoveCodeRedundancies>True</RemoveCodeRedundancies><CSUseAutoProperty>True</CSUseAutoProperty><CSMakeFieldReadonly>True</CSMakeFieldReadonly><CSMakeAutoPropertyGetOnly>True</CSMakeAutoPropertyGetOnly><CSArrangeQualifiers>True</CSArrangeQualifiers><CSFixBuiltinTypeReferences>True</CSFixBuiltinTypeReferences><CssReformatCode>True</CssReformatCode><VBReformatCode>True</VBReformatCode><VBFormatDocComments>True</VBFormatDocComments><CSOptimizeUsings><OptimizeUsings>True</OptimizeUsings></CSOptimizeUsings><CSReformatCode>True</CSReformatCode><CSharpFormatDocComments>True</CSharpFormatDocComments><IDEA_SETTINGS><profile version="1.0">
<option name="myName" value="Nitrox" />
-</profile></IDEA_SETTINGS><CSShortenReferences>True</CSShortenReferences></Profile>
+</profile></IDEA_SETTINGS><CSShortenReferences>True</CSShortenReferences><RIDER_SETTINGS><profile>
+ <Language id="CSS">
+ <Rearrange>true</Rearrange>
+ <Reformat>true</Reformat>
+ </Language>
+ <Language id="EditorConfig">
+ <Reformat>true</Reformat>
+ </Language>
+ <Language id="HTML">
+ <OptimizeImports>true</OptimizeImports>
+ <Rearrange>true</Rearrange>
+ <Reformat>true</Reformat>
+ </Language>
+ <Language id="HTTP Request">
+ <Reformat>true</Reformat>
+ </Language>
+ <Language id="Handlebars">
+ <Reformat>true</Reformat>
+ </Language>
+ <Language id="Ini">
+ <Reformat>true</Reformat>
+ </Language>
+ <Language id="JSON">
+ <Reformat>true</Reformat>
+ </Language>
+ <Language id="Jade">
+ <Reformat>true</Reformat>
+ </Language>
+ <Language id="JavaScript">
+ <OptimizeImports>true</OptimizeImports>
+ <Rearrange>true</Rearrange>
+ <Reformat>true</Reformat>
+ </Language>
+ <Language id="Markdown">
+ <Reformat>true</Reformat>
+ </Language>
+ <Language id="Properties">
+ <Reformat>true</Reformat>
+ </Language>
+ <Language id="RELAX-NG">
+ <Reformat>true</Reformat>
+ </Language>
+ <Language id="SQL">
+ <Reformat>true</Reformat>
+ </Language>
+ <Language id="XML">
+ <OptimizeImports>true</OptimizeImports>
+ <Rearrange>true</Rearrange>
+ <Reformat>true</Reformat>
+ </Language>
+ <Language id="yaml">
+ <Reformat>true</Reformat>
+ </Language>
+</profile></RIDER_SETTINGS></Profile>
Built-in: Full Cleanup
Nitrox
True
@@ -27,6 +79,7 @@
False
CHOP_IF_LONG
256
+ True
CHOP_IF_LONG
200
False
diff --git a/NitroxClient/Communication/Packets/Processors/RocketPreflightCompleteProcessor.cs b/NitroxClient/Communication/Packets/Processors/RocketPreflightCompleteProcessor.cs
index c7552c11fb..1a9972a586 100644
--- a/NitroxClient/Communication/Packets/Processors/RocketPreflightCompleteProcessor.cs
+++ b/NitroxClient/Communication/Packets/Processors/RocketPreflightCompleteProcessor.cs
@@ -3,6 +3,7 @@
using NitroxClient.MonoBehaviours;
using NitroxModel_Subnautica.Packets;
using UnityEngine;
+using NitroxClient.Unity.Helper;
namespace NitroxClient.Communication.Packets.Processors
{
@@ -54,7 +55,7 @@ public override void Process(RocketPreflightComplete packet)
{
if (!throwSwitch.completed && throwSwitch.preflightCheck == packet.FlightCheck)
{
- throwSwitch.animator?.SetTrigger("Throw");
+ throwSwitch.animator.AliveOrNull()?.SetTrigger("Throw");
throwSwitch.completed = true;
CompletePreflightCheck(throwSwitch.preflightCheckSwitch);
throwSwitch.cinematicTrigger.showIconOnHandHover = false;
@@ -72,7 +73,7 @@ private void CompletePreflightCheck(PreflightCheckSwitch preflightCheckSwitch)
{
using (packetSender.Suppress())
{
- preflightCheckSwitch.preflightCheckManager?.CompletePreflightCheck(preflightCheckSwitch.preflightCheck);
+ preflightCheckSwitch.preflightCheckManager.AliveOrNull()?.CompletePreflightCheck(preflightCheckSwitch.preflightCheck);
}
}
}
diff --git a/NitroxClient/GameLogic/Vehicles.cs b/NitroxClient/GameLogic/Vehicles.cs
index 7dc1a362b5..407fc481a3 100644
--- a/NitroxClient/GameLogic/Vehicles.cs
+++ b/NitroxClient/GameLogic/Vehicles.cs
@@ -269,7 +269,7 @@ public void UpdateVehiclePosition(VehicleMovementData vehicleModel, Optional());
+ playerInstance.SetPilotingChair(subRoot.AliveOrNull()?.GetComponentInChildren());
playerInstance.AnimationController.UpdatePlayerAnimations = false;
}
}
@@ -308,13 +308,13 @@ private void OnVehiclePrefabLoaded(TechType techType, GameObject prefab, NitroxI
if (!string.IsNullOrEmpty(name))
{
vehicle.vehicleName = name;
- vehicle.subName?.DeserializeName(vehicle.vehicleName);
+ vehicle.subName.AliveOrNull()?.DeserializeName(vehicle.vehicleName);
}
if (hsb != null)
{
vehicle.vehicleColors = hsb;
- vehicle.subName?.DeserializeColors(hsb);
+ vehicle.subName.AliveOrNull()?.DeserializeColors(hsb);
}
vehicle.GetComponent().health = health;
@@ -340,13 +340,13 @@ private void OnVehiclePrefabLoaded(TechType techType, GameObject prefab, NitroxI
if (!string.IsNullOrEmpty(name))
{
rocket.rocketName = name;
- rocket.subName?.DeserializeName(name);
+ rocket.subName.AliveOrNull()?.DeserializeName(name);
}
if (hsb != null)
{
rocket.rocketColors = hsb;
- rocket.subName?.DeserializeColors(hsb);
+ rocket.subName.AliveOrNull()?.DeserializeColors(hsb);
}
}
diff --git a/NitroxClient/MonoBehaviours/FMODEmitterController.cs b/NitroxClient/MonoBehaviours/FMODEmitterController.cs
index bf312bd027..b3dcc769ef 100644
--- a/NitroxClient/MonoBehaviours/FMODEmitterController.cs
+++ b/NitroxClient/MonoBehaviours/FMODEmitterController.cs
@@ -2,6 +2,7 @@
using FMOD.Studio;
using FMODUnity;
using UnityEngine;
+using NitroxClient.Unity.Helper;
#pragma warning disable 618
namespace NitroxClient.MonoBehaviours
@@ -56,12 +57,12 @@ public void AddEmitter(string path, FMOD_StudioEventEmitter studioEmitter, float
}
}
- public void PlayCustomEmitter(string path) => customEmitters[path]?.Play();
- public void ParamCustomEmitter(string path, int paramIndex, float value) => customEmitters[path]?.SetParameterValue(paramIndex, value);
- public void StopCustomEmitter(string path) => customEmitters[path]?.Stop();
+ public void PlayCustomEmitter(string path) => customEmitters[path].AliveOrNull()?.Play();
+ public void ParamCustomEmitter(string path, int paramIndex, float value) => customEmitters[path].AliveOrNull()?.SetParameterValue(paramIndex, value);
+ public void StopCustomEmitter(string path) => customEmitters[path].AliveOrNull()?.Stop();
- public void PlayStudioEmitter(string path) => studioEmitters[path]?.PlayUI();
- public void StopStudioEmitter(string path, bool allowFadeout) => studioEmitters[path]?.Stop(allowFadeout);
+ public void PlayStudioEmitter(string path) => studioEmitters[path].AliveOrNull()?.PlayUI();
+ public void StopStudioEmitter(string path, bool allowFadeout) => studioEmitters[path].AliveOrNull()?.Stop(allowFadeout);
public void PlayCustomLoopingEmitter(string path)
{
diff --git a/NitroxModel/Helper/Validate.cs b/NitroxModel/Helper/Validate.cs
index f1733b146f..79b4170936 100644
--- a/NitroxModel/Helper/Validate.cs
+++ b/NitroxModel/Helper/Validate.cs
@@ -1,5 +1,7 @@
-using System;
+extern alias JB;
+using System;
using System.Runtime.CompilerServices;
+using JB::JetBrains.Annotations;
using NitroxModel.DataStructures.Util;
namespace NitroxModel.Helper;
@@ -8,6 +10,7 @@ public static class Validate
{
// "where T : class" prevents non-nullable valuetypes from getting boxed to objects.
// In other words: Error when trying to assert non-null on something that can't be null in the first place.
+ [ContractAnnotation("o:null => halt")]
public static void NotNull(T o, [CallerArgumentExpression("o")] string argumentExpression = null) where T : class
{
if (o != null)
diff --git a/NitroxModel/NitroxModel.csproj b/NitroxModel/NitroxModel.csproj
index f2e2edb6b0..3fdf40a726 100644
--- a/NitroxModel/NitroxModel.csproj
+++ b/NitroxModel/NitroxModel.csproj
@@ -8,13 +8,6 @@
-
- all
-
-
- all
- runtime; build; native; contentfiles; analyzers; buildtransitive
-
@@ -42,5 +35,4 @@
-
diff --git a/NitroxPatcher/Main.cs b/NitroxPatcher/Main.cs
index 67f3ee44ef..caabf8e70c 100644
--- a/NitroxPatcher/Main.cs
+++ b/NitroxPatcher/Main.cs
@@ -1,9 +1,10 @@
-global using NitroxModel.Logger;
+extern alias JB;
+global using NitroxModel.Logger;
using System;
using System.IO;
using System.Reflection;
using System.Runtime.CompilerServices;
-using JetBrains.Annotations;
+using JB::JetBrains.Annotations;
using Microsoft.Win32;
using NitroxModel.Helper;
using NitroxModel_Subnautica.Logger;
diff --git a/NitroxServer-Subnautica/Serialization/Resources/ResourceAssets.cs b/NitroxServer-Subnautica/Serialization/Resources/ResourceAssets.cs
index 583b1ca026..0b25407f6c 100644
--- a/NitroxServer-Subnautica/Serialization/Resources/ResourceAssets.cs
+++ b/NitroxServer-Subnautica/Serialization/Resources/ResourceAssets.cs
@@ -9,16 +9,16 @@ namespace NitroxServer_Subnautica.Serialization.Resources
{
public class ResourceAssets
{
- public Dictionary WorldEntitiesByClassId { get; } = new Dictionary();
- public Dictionary PrefabsByClassId { get; } = new Dictionary();
+ public Dictionary WorldEntitiesByClassId { get; } = new();
+ public Dictionary PrefabsByClassId { get; } = new();
public string LootDistributionsJson { get; set; } = "";
- public Dictionary PrefabPlaceholderGroupsByGroupClassId = new Dictionary();
+ public readonly Dictionary PrefabPlaceholderGroupsByGroupClassId = new();
public RandomStartGenerator NitroxRandom;
public static void ValidateMembers(ResourceAssets resourceAssets)
{
- Validate.IsFalse(resourceAssets == null);
+ Validate.NotNull(resourceAssets);
Validate.IsTrue(resourceAssets.WorldEntitiesByClassId.Count > 0);
Validate.IsTrue(resourceAssets.LootDistributionsJson != "");
Validate.IsTrue(resourceAssets.PrefabPlaceholderGroupsByGroupClassId.Count > 0);