Skip to content

Commit bbdaa8c

Browse files
committed
Squashing commits because Linux was putting my real fucking name on them
1 parent 5bf3dcb commit bbdaa8c

449 files changed

Lines changed: 44540 additions & 29077 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.editorconfig

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -244,6 +244,13 @@ dotnet_diagnostic.MA0026.severity = warning
244244
dotnet_diagnostic.MA0046.severity = suggestion
245245
dotnet_diagnostic.MA0051.severity = suggestion
246246
dotnet_diagnostic.MA0011.severity = suggestion
247+
# MA0008 (Add StructLayoutAttribute) is noise for managed domain value-structs, whose default
248+
# auto-layout is what we want; silenced so Core (on the TreatWarningsAsErrors ratchet) stays clean.
249+
dotnet_diagnostic.MA0008.severity = silent
250+
dotnet_diagnostic.MA0048.severity = silent
251+
dotnet_diagnostic.S3267.severity = silent
252+
dotnet_diagnostic.S6605.severity = none
253+
dotnet_diagnostic.S6678.severity = none
247254

248255
[*.{cs,vb}]
249256
dotnet_style_operator_placement_when_wrapping = beginning_of_line
@@ -270,4 +277,4 @@ dotnet_style_predefined_type_for_locals_parameters_members = true:silent
270277
dotnet_style_predefined_type_for_member_access = true:silent
271278
dotnet_style_require_accessibility_modifiers = for_non_interface_members:silent
272279
dotnet_style_allow_multiple_blank_lines_experimental = true:silent
273-
dotnet_style_allow_statement_immediately_after_block_experimental = true:silent
280+
dotnet_style_allow_statement_immediately_after_block_experimental = true:silent

.github/workflows/build.yml

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,4 +31,17 @@ jobs:
3131
uses: actions/upload-artifact@v4
3232
with:
3333
path: |
34-
./Snowcloak/bin/*
34+
./Snowcloak/bin/*
35+
36+
test:
37+
runs-on: ubuntu-latest
38+
steps:
39+
- uses: actions/checkout@v4
40+
with:
41+
submodules: true
42+
- name: Setup .NET
43+
uses: actions/setup-dotnet@v4
44+
with:
45+
dotnet-version: '10.x'
46+
- name: Unit tests (Core + Infrastructure)
47+
run: dotnet test Snowcloak.Tests/Snowcloak.Tests.csproj --configuration Release --nologo

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -348,3 +348,5 @@ MigrationBackup/
348348

349349
# Ionide (cross platform F# VS Code tools) working folder
350350
.ionide/
351+
352+
docs/

Directory.Build.props

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<Project>
3+
4+
<PropertyGroup>
5+
<Nullable>enable</Nullable>
6+
<ImplicitUsings>enable</ImplicitUsings>
7+
<LangVersion>latest</LangVersion>
8+
9+
<EnableWindowsTargeting>true</EnableWindowsTargeting>
10+
<TreatWarningsAsErrors>false</TreatWarningsAsErrors>
11+
</PropertyGroup>
12+
13+
<PropertyGroup Condition="'$(Configuration)' == 'Release'">
14+
<RunAnalyzersDuringBuild>true</RunAnalyzersDuringBuild>
15+
<EnforceCodeStyleInBuild>true</EnforceCodeStyleInBuild>
16+
<AnalysisLevel>latest-all</AnalysisLevel>
17+
<AnalysisMode>AllEnabledByDefault</AnalysisMode>
18+
</PropertyGroup>
19+
20+
<ItemGroup>
21+
<PackageReference Include="Meziantou.Analyzer" Version="3.0.104">
22+
<PrivateAssets>all</PrivateAssets>
23+
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
24+
</PackageReference>
25+
<PackageReference Include="SonarAnalyzer.CSharp" Version="10.27.0.140913">
26+
<PrivateAssets>all</PrivateAssets>
27+
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
28+
</PackageReference>
29+
</ItemGroup>
30+
31+
</Project>
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
namespace Snowcloak.Core.Accounts;
2+
3+
/// <summary>
4+
/// Pure validation for the Snowcloak account username/password credential forms shared by
5+
/// the onboarding (IntroUI) and Settings account flows. Returns a user-facing message describing
6+
/// the first problem found, or <c>null</c> when the credentials are acceptable.
7+
/// </summary>
8+
public static class AccountCredentialValidator
9+
{
10+
public static string? Validate(string? username, string? password, string? passwordConfirm, bool requireConfirmation)
11+
{
12+
username ??= string.Empty;
13+
password ??= string.Empty;
14+
passwordConfirm ??= string.Empty;
15+
16+
if (username.Trim().Length == 0)
17+
return "Enter a username.";
18+
19+
if (username.Trim().Length is < 3 or > 64)
20+
return "Username must contain between 3 and 64 characters.";
21+
22+
if (username.Any(char.IsWhiteSpace))
23+
return "Username cannot contain spaces.";
24+
25+
if (string.IsNullOrEmpty(password))
26+
return "Enter a password.";
27+
28+
if (!requireConfirmation)
29+
return null;
30+
31+
if (password.Length < 12)
32+
return "New account passwords must be at least 12 characters long.";
33+
34+
if (string.IsNullOrEmpty(passwordConfirm))
35+
return "Re-enter your password to confirm it.";
36+
37+
return string.Equals(password, passwordConfirm, StringComparison.Ordinal)
38+
? null
39+
: "The password confirmation does not match.";
40+
}
41+
}
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
using Snowcloak.API.Dto.Account;
2+
3+
namespace Snowcloak.Core.Accounts;
4+
5+
public sealed record AccountEntitlements(
6+
bool IsLinked = false,
7+
bool IsPayingPatron = false,
8+
bool HasBenefits = false,
9+
bool IsCompetitionWinner = false,
10+
bool IsTestOverride = false,
11+
bool IsCreatorForCampaign = false,
12+
string PatreonUserId = "")
13+
{
14+
public static AccountEntitlements Empty { get; } = new();
15+
}
16+
17+
public static class AccountEntitlementMapper
18+
{
19+
public static AccountEntitlements FromPatreonStatus(PatreonStatusReplyDto? dto)
20+
{
21+
return dto == null
22+
? AccountEntitlements.Empty
23+
: FromDto(dto.Entitlements, dto.IsLinked, dto.IsPayingPatron, dto.HasBenefits,
24+
dto.IsCompetitionWinner, dto.IsTestOverride, dto.IsCreatorForCampaign, dto.PatreonUserId);
25+
}
26+
27+
public static AccountEntitlements FromPatreonLinkPoll(PatreonLinkPollReplyDto? dto)
28+
{
29+
return dto == null
30+
? AccountEntitlements.Empty
31+
: FromDto(dto.Entitlements, dto.IsLinked, dto.IsPayingPatron, dto.HasBenefits,
32+
dto.IsCompetitionWinner, dto.IsTestOverride, dto.IsCreatorForCampaign, null);
33+
}
34+
35+
public static AccountEntitlements FromDto(AccountEntitlementsDto? dto)
36+
{
37+
return FromDto(dto, false, false, false, false, false, false, null);
38+
}
39+
40+
public static AccountEntitlements FromDto(AccountEntitlementsDto? dto, bool fallbackIsLinked,
41+
bool fallbackIsPayingPatron, bool fallbackHasBenefits, bool fallbackIsCompetitionWinner,
42+
bool fallbackIsTestOverride, bool fallbackIsCreatorForCampaign, string? fallbackPatreonUserId)
43+
{
44+
var hasFallback = fallbackIsLinked
45+
|| fallbackIsPayingPatron
46+
|| fallbackHasBenefits
47+
|| fallbackIsCompetitionWinner
48+
|| fallbackIsTestOverride
49+
|| fallbackIsCreatorForCampaign
50+
|| !string.IsNullOrWhiteSpace(fallbackPatreonUserId);
51+
52+
if (dto != null && (HasValues(dto) || !hasFallback))
53+
{
54+
return new AccountEntitlements(
55+
dto.IsLinked,
56+
dto.IsPayingPatron,
57+
dto.HasBenefits,
58+
dto.IsCompetitionWinner,
59+
dto.IsTestOverride,
60+
dto.IsCreatorForCampaign,
61+
dto.PatreonUserId ?? string.Empty);
62+
}
63+
64+
return new AccountEntitlements(
65+
fallbackIsLinked,
66+
fallbackIsPayingPatron,
67+
fallbackHasBenefits,
68+
fallbackIsCompetitionWinner,
69+
fallbackIsTestOverride,
70+
fallbackIsCreatorForCampaign,
71+
fallbackPatreonUserId ?? string.Empty);
72+
}
73+
74+
private static bool HasValues(AccountEntitlementsDto dto)
75+
{
76+
return dto.IsLinked
77+
|| dto.IsPayingPatron
78+
|| dto.HasBenefits
79+
|| dto.IsCompetitionWinner
80+
|| dto.IsTestOverride
81+
|| dto.IsCreatorForCampaign
82+
|| !string.IsNullOrWhiteSpace(dto.PatreonUserId);
83+
}
84+
}
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
using Snowcloak.API.Dto.Account;
2+
3+
namespace Snowcloak.Core.Accounts;
4+
5+
public static class AccountSecretKeySelectionPolicy
6+
{
7+
public static int GetAssignmentRank(AccountSecretKeyDto key, string? preferredUid)
8+
{
9+
ArgumentNullException.ThrowIfNull(key);
10+
11+
var rank = string.Equals(key.Source, "generated", StringComparison.OrdinalIgnoreCase) ? 1 : 0;
12+
if (!string.IsNullOrWhiteSpace(preferredUid)
13+
&& string.Equals(key.Uid, preferredUid.Trim(), StringComparison.OrdinalIgnoreCase))
14+
{
15+
rank += 2;
16+
}
17+
18+
return rank;
19+
}
20+
21+
public static DateTimeOffset GetActivityTime(AccountSecretKeyDto key)
22+
{
23+
ArgumentNullException.ThrowIfNull(key);
24+
return key.LastUsedAtUtc ?? key.CreatedAtUtc;
25+
}
26+
27+
public static bool IsBetterAssignmentCandidate(AccountSecretKeyDto candidate, AccountSecretKeyDto? current,
28+
string? preferredUid)
29+
{
30+
ArgumentNullException.ThrowIfNull(candidate);
31+
if (current == null)
32+
{
33+
return true;
34+
}
35+
36+
var candidateRank = GetAssignmentRank(candidate, preferredUid);
37+
var currentRank = GetAssignmentRank(current, preferredUid);
38+
if (candidateRank != currentRank)
39+
{
40+
return candidateRank > currentRank;
41+
}
42+
43+
return GetActivityTime(candidate) > GetActivityTime(current);
44+
}
45+
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
namespace Snowcloak.Core.Analysis;
2+
3+
public sealed record AnalysisFileEntry
4+
{
5+
public AnalysisFileEntry(
6+
string hash,
7+
string fileType,
8+
IEnumerable<string> gamePaths,
9+
IEnumerable<string> filePaths,
10+
long originalSize,
11+
long compressedSize,
12+
long triangles,
13+
AnalysisTextureTraits? textureTraits = null)
14+
{
15+
ArgumentNullException.ThrowIfNull(hash);
16+
ArgumentNullException.ThrowIfNull(fileType);
17+
ArgumentNullException.ThrowIfNull(gamePaths);
18+
ArgumentNullException.ThrowIfNull(filePaths);
19+
20+
Hash = hash;
21+
FileType = fileType;
22+
GamePaths = Array.AsReadOnly(gamePaths.ToArray());
23+
FilePaths = Array.AsReadOnly(filePaths.ToArray());
24+
OriginalSize = originalSize;
25+
CompressedSize = compressedSize;
26+
Triangles = triangles;
27+
TextureTraits = textureTraits;
28+
}
29+
30+
public string Hash { get; }
31+
public string FileType { get; }
32+
public IReadOnlyList<string> GamePaths { get; }
33+
public IReadOnlyList<string> FilePaths { get; }
34+
public long OriginalSize { get; }
35+
public long CompressedSize { get; }
36+
public long Triangles { get; }
37+
public AnalysisTextureTraits? TextureTraits { get; }
38+
public bool IsComputed => OriginalSize > 0 && CompressedSize > 0;
39+
public string FormatSummary => TextureTraits?.FormatSummary ?? string.Empty;
40+
public bool IsRiskyTexture => TextureTraits?.IsRisky ?? false;
41+
42+
public AnalysisFileEntry WithSizes(long originalSize, long compressedSize)
43+
=> new(Hash, FileType, GamePaths, FilePaths, originalSize, compressedSize, Triangles, TextureTraits);
44+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
using System.Collections.ObjectModel;
2+
using Snowcloak.API.Data.Enum;
3+
4+
namespace Snowcloak.Core.Analysis;
5+
6+
public sealed class AnalysisObjectSnapshot
7+
{
8+
public AnalysisObjectSnapshot(ObjectKind kind, IEnumerable<AnalysisFileEntry> files)
9+
{
10+
ArgumentNullException.ThrowIfNull(files);
11+
12+
Kind = kind;
13+
var snapshot = new Dictionary<string, AnalysisFileEntry>(StringComparer.OrdinalIgnoreCase);
14+
foreach (var file in files)
15+
{
16+
snapshot[file.Hash] = file;
17+
}
18+
Files = new ReadOnlyDictionary<string, AnalysisFileEntry>(snapshot);
19+
}
20+
21+
public ObjectKind Kind { get; }
22+
public IReadOnlyDictionary<string, AnalysisFileEntry> Files { get; }
23+
}
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
using System.Collections.ObjectModel;
2+
using Snowcloak.API.Data.Enum;
3+
4+
namespace Snowcloak.Core.Analysis;
5+
6+
public sealed class AnalysisSnapshot
7+
{
8+
private static readonly IReadOnlyDictionary<ObjectKind, AnalysisObjectSnapshot> EmptyObjects =
9+
new ReadOnlyDictionary<ObjectKind, AnalysisObjectSnapshot>(new Dictionary<ObjectKind, AnalysisObjectSnapshot>());
10+
11+
public static AnalysisSnapshot Empty { get; } = new(EmptyObjects);
12+
13+
public AnalysisSnapshot(IEnumerable<AnalysisObjectSnapshot> objects)
14+
: this(new ReadOnlyDictionary<ObjectKind, AnalysisObjectSnapshot>(
15+
ValidateObjects(objects).ToDictionary(obj => obj.Kind, obj => obj)))
16+
{
17+
}
18+
19+
private AnalysisSnapshot(IReadOnlyDictionary<ObjectKind, AnalysisObjectSnapshot> objects)
20+
{
21+
Objects = objects;
22+
}
23+
24+
public IReadOnlyDictionary<ObjectKind, AnalysisObjectSnapshot> Objects { get; }
25+
public bool IsEmpty => Objects.Count == 0;
26+
public IEnumerable<AnalysisFileEntry> Files => Objects.Values.SelectMany(obj => obj.Files.Values);
27+
28+
public AnalysisSnapshot UpdateFiles(IEnumerable<AnalysisFileEntry> files)
29+
{
30+
ArgumentNullException.ThrowIfNull(files);
31+
32+
var updates = new Dictionary<string, AnalysisFileEntry>(StringComparer.OrdinalIgnoreCase);
33+
foreach (var file in files)
34+
{
35+
updates[file.Hash] = file;
36+
}
37+
if (updates.Count == 0) return this;
38+
39+
return new AnalysisSnapshot(Objects.Values.Select(obj =>
40+
new AnalysisObjectSnapshot(obj.Kind, obj.Files.Values.Select(file =>
41+
updates.TryGetValue(file.Hash, out var updated) ? updated : file))));
42+
}
43+
44+
private static IEnumerable<AnalysisObjectSnapshot> ValidateObjects(IEnumerable<AnalysisObjectSnapshot> objects)
45+
{
46+
ArgumentNullException.ThrowIfNull(objects);
47+
return objects;
48+
}
49+
}

0 commit comments

Comments
 (0)