first commit

This commit is contained in:
2025-07-06 00:23:46 +02:00
commit 38f50c8819
1788 changed files with 112878 additions and 0 deletions

View File

@@ -0,0 +1,29 @@
using System;
using System.ComponentModel.DataAnnotations;
using System.IO;
using Nitrox.Launcher.Models.Design;
namespace Nitrox.Launcher.Models.Validators;
/// <summary>
/// Checks that value is a usable <see cref="BackupItem" />.
/// </summary>
public sealed class BackupAttribute : TypedValidationAttribute<BackupItem>
{
protected override ValidationResult IsValid(BackupItem value, ValidationContext context)
{
if (value == null)
{
return new ValidationResult($"{context.DisplayName} must not be null.");
}
if (value.BackupFileName == null || value.BackupFileName.AsSpan().Trim().IsEmpty)
{
return new ValidationResult($"{context.DisplayName} must have a backup path assigned");
}
if (!File.Exists(value.BackupFileName))
{
return new ValidationResult($"{context.DisplayName} must point to a valid file.");
}
return ValidationResult.Success;
}
}

View File

@@ -0,0 +1,28 @@
using System.ComponentModel.DataAnnotations;
using System.IO;
using System.Linq;
namespace Nitrox.Launcher.Models.Validators;
/// <summary>
/// Tests that the value is usable as file name (excluding validity as file path or file extension).
/// </summary>
public sealed class FileNameAttribute : TypedValidationAttribute<string>
{
internal static readonly char[] InvalidPathCharacters = Path.GetInvalidFileNameChars();
protected override ValidationResult IsValid(string value, ValidationContext context)
{
if (value == null)
{
return ValidationResult.Success;
}
int indexOfAny = value.IndexOfAny(InvalidPathCharacters);
if (indexOfAny > -1)
{
return new ValidationResult($"{context.DisplayName} must not contain '{value[indexOfAny]}'. All invalid characters: {string.Join(' ', InvalidPathCharacters.Where(c => c > 31))}");
}
return ValidationResult.Success;
}
}

View File

@@ -0,0 +1,58 @@
using System;
using System.ComponentModel.DataAnnotations;
using System.IO;
namespace Nitrox.Launcher.Models.Validators;
/// <summary>
/// Tests that the save name doesn't conflict with other Nitrox saves.
/// </summary>
public sealed class NitroxUniqueSaveName : TypedValidationAttribute<string>
{
public string SavesFolderDirPropertyName { get; }
public bool AllowCaseInsensitiveName { get; }
public string OriginalValuePropertyName { get; }
public NitroxUniqueSaveName(string savesFolderDirPropertyName, bool allowCaseInsensitiveName = false, string originalValuePropertyName = null)
{
ArgumentException.ThrowIfNullOrWhiteSpace(savesFolderDirPropertyName);
SavesFolderDirPropertyName = savesFolderDirPropertyName;
AllowCaseInsensitiveName = allowCaseInsensitiveName;
OriginalValuePropertyName = originalValuePropertyName;
}
protected override ValidationResult IsValid(string value, ValidationContext context)
{
static bool SaveFolderExists(string folderName, bool matchExact, string savesFolderDir)
{
if (!matchExact)
{
foreach (string dir in Directory.EnumerateDirectories(savesFolderDir))
{
if (Path.GetFileName(dir).Equals(folderName, StringComparison.Ordinal))
{
return true;
}
}
return false;
}
return Path.Exists(Path.Combine(savesFolderDir, folderName));
}
if (!Directory.Exists(ReadProperty<string>(context, SavesFolderDirPropertyName)))
{
return ValidationResult.Success;
}
if (!string.IsNullOrEmpty(OriginalValuePropertyName) && value == ReadProperty<string>(context, OriginalValuePropertyName))
{
return ValidationResult.Success;
}
if (SaveFolderExists(value, !AllowCaseInsensitiveName, ReadProperty<string>(context, SavesFolderDirPropertyName)))
{
return new ValidationResult($@"Save ""{value}"" already exists.");
}
return ValidationResult.Success;
}
}

View File

@@ -0,0 +1,20 @@
using System.ComponentModel.DataAnnotations;
using System.Text.RegularExpressions;
namespace Nitrox.Launcher.Models.Validators;
public sealed partial class NitroxWorldSeedAttribute : TypedValidationAttribute<string>
{
[GeneratedRegex(@"^[a-zA-Z]{10}$")]
private static partial Regex NitroxWorldSeedRegex { get; }
protected override ValidationResult IsValid(string value, ValidationContext context)
{
if (string.IsNullOrEmpty(value) || NitroxWorldSeedRegex.IsMatch(value))
{
return ValidationResult.Success;
}
return new ValidationResult($"The field {context.DisplayName} must contain 10 alphabetical characters.");
}
}

View File

@@ -0,0 +1,19 @@
using System;
using System.ComponentModel.DataAnnotations;
namespace Nitrox.Launcher.Models.Validators;
/// <summary>
/// Tests that the value doesn't end with the specified text.
/// </summary>
public sealed class NotEndsWithAttribute(string text, StringComparison comparison = StringComparison.OrdinalIgnoreCase) : TypedValidationAttribute<string>
{
protected override ValidationResult IsValid(string value, ValidationContext context)
{
if (value == null)
{
return ValidationResult.Success;
}
return value.EndsWith(text, comparison) ? new ValidationResult($"{context.DisplayName} must not contain the text '{text}' at the end.") : ValidationResult.Success;
}
}

View File

@@ -0,0 +1,31 @@
using System;
using System.ComponentModel.DataAnnotations;
namespace Nitrox.Launcher.Models.Validators;
/// <summary>Validates a Nitrox save name.</summary>
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Parameter)]
public class SaveNameAttribute : DataTypeAttribute
{
public SaveNameAttribute() : base(DataType.Text)
{
}
public override bool IsValid(object value)
{
if (value is not string str)
{
return false;
}
int indexOfAny = str.IndexOfAny(FileNameAttribute.InvalidPathCharacters);
if (indexOfAny > -1)
{
return false;
}
if (str.EndsWith(".", StringComparison.OrdinalIgnoreCase))
{
return false;
}
return true;
}
}

View File

@@ -0,0 +1,32 @@
using System.ComponentModel.DataAnnotations;
using System.Reflection;
namespace Nitrox.Launcher.Models.Validators;
public abstract class TypedValidationAttribute<T> : ValidationAttribute
{
protected abstract ValidationResult IsValid(T value, ValidationContext context);
protected override ValidationResult IsValid(object value, ValidationContext context)
{
if (value == default)
{
return IsValid(default, context);
}
if (value is not T typedValue)
{
return new ValidationResult($"The field {context.DisplayName} must be of type {typeof(T).Name}.");
}
return IsValid(typedValue, context);
}
protected static TResult ReadProperty<TResult>(ValidationContext context, string propertyName)
{
if (string.IsNullOrWhiteSpace(propertyName))
{
return default;
}
object value = context.ObjectType.GetProperty(propertyName, BindingFlags.Instance | BindingFlags.Public | BindingFlags.Static | BindingFlags.NonPublic)?.GetValue(context.ObjectInstance);
return value is TResult tValue ? tValue : default;
}
}