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>&lt;profile version="1.0"&gt; - &lt;option name="myName" value="Copy of Built-in: Full Cleanup" /&gt; -&lt;/profile&gt;</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>&lt;profile version="1.0"&gt; + 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>&lt;profile version="1.0"&gt; &lt;option name="myName" value="Nitrox" /&gt; -&lt;/profile&gt;</IDEA_SETTINGS><CSShortenReferences>True</CSShortenReferences></Profile> +&lt;/profile&gt;</IDEA_SETTINGS><CSShortenReferences>True</CSShortenReferences><RIDER_SETTINGS>&lt;profile&gt; + &lt;Language id="CSS"&gt; + &lt;Rearrange&gt;true&lt;/Rearrange&gt; + &lt;Reformat&gt;true&lt;/Reformat&gt; + &lt;/Language&gt; + &lt;Language id="EditorConfig"&gt; + &lt;Reformat&gt;true&lt;/Reformat&gt; + &lt;/Language&gt; + &lt;Language id="HTML"&gt; + &lt;OptimizeImports&gt;true&lt;/OptimizeImports&gt; + &lt;Rearrange&gt;true&lt;/Rearrange&gt; + &lt;Reformat&gt;true&lt;/Reformat&gt; + &lt;/Language&gt; + &lt;Language id="HTTP Request"&gt; + &lt;Reformat&gt;true&lt;/Reformat&gt; + &lt;/Language&gt; + &lt;Language id="Handlebars"&gt; + &lt;Reformat&gt;true&lt;/Reformat&gt; + &lt;/Language&gt; + &lt;Language id="Ini"&gt; + &lt;Reformat&gt;true&lt;/Reformat&gt; + &lt;/Language&gt; + &lt;Language id="JSON"&gt; + &lt;Reformat&gt;true&lt;/Reformat&gt; + &lt;/Language&gt; + &lt;Language id="Jade"&gt; + &lt;Reformat&gt;true&lt;/Reformat&gt; + &lt;/Language&gt; + &lt;Language id="JavaScript"&gt; + &lt;OptimizeImports&gt;true&lt;/OptimizeImports&gt; + &lt;Rearrange&gt;true&lt;/Rearrange&gt; + &lt;Reformat&gt;true&lt;/Reformat&gt; + &lt;/Language&gt; + &lt;Language id="Markdown"&gt; + &lt;Reformat&gt;true&lt;/Reformat&gt; + &lt;/Language&gt; + &lt;Language id="Properties"&gt; + &lt;Reformat&gt;true&lt;/Reformat&gt; + &lt;/Language&gt; + &lt;Language id="RELAX-NG"&gt; + &lt;Reformat&gt;true&lt;/Reformat&gt; + &lt;/Language&gt; + &lt;Language id="SQL"&gt; + &lt;Reformat&gt;true&lt;/Reformat&gt; + &lt;/Language&gt; + &lt;Language id="XML"&gt; + &lt;OptimizeImports&gt;true&lt;/OptimizeImports&gt; + &lt;Rearrange&gt;true&lt;/Rearrange&gt; + &lt;Reformat&gt;true&lt;/Reformat&gt; + &lt;/Language&gt; + &lt;Language id="yaml"&gt; + &lt;Reformat&gt;true&lt;/Reformat&gt; + &lt;/Language&gt; +&lt;/profile&gt;</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);