Skip to content

Commit df25caf

Browse files
committed
feat: impl basically function
1 parent e2d66a9 commit df25caf

File tree

4 files changed

+335
-0
lines changed

4 files changed

+335
-0
lines changed

CS2.Util.ModVerifier.sln

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
2+
Microsoft Visual Studio Solution File, Format Version 12.00
3+
# Visual Studio Version 17
4+
VisualStudioVersion = 17.14.35806.103 d17.14
5+
MinimumVisualStudioVersion = 10.0.40219.1
6+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CS2.Util.ModVerifier", "CS2.Util.ModVerifier\CS2.Util.ModVerifier.csproj", "{56407534-18DF-4215-A7AD-A478772F174F}"
7+
EndProject
8+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Spectre.Console", "..\spectre.console\src\Spectre.Console\Spectre.Console.csproj", "{EEB7710D-8D36-577B-90CF-7D20A60FF533}"
9+
EndProject
10+
Global
11+
GlobalSection(SolutionConfigurationPlatforms) = preSolution
12+
Debug|Any CPU = Debug|Any CPU
13+
Release|Any CPU = Release|Any CPU
14+
EndGlobalSection
15+
GlobalSection(ProjectConfigurationPlatforms) = postSolution
16+
{56407534-18DF-4215-A7AD-A478772F174F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
17+
{56407534-18DF-4215-A7AD-A478772F174F}.Debug|Any CPU.Build.0 = Debug|Any CPU
18+
{56407534-18DF-4215-A7AD-A478772F174F}.Release|Any CPU.ActiveCfg = Release|Any CPU
19+
{56407534-18DF-4215-A7AD-A478772F174F}.Release|Any CPU.Build.0 = Release|Any CPU
20+
{EEB7710D-8D36-577B-90CF-7D20A60FF533}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
21+
{EEB7710D-8D36-577B-90CF-7D20A60FF533}.Debug|Any CPU.Build.0 = Debug|Any CPU
22+
{EEB7710D-8D36-577B-90CF-7D20A60FF533}.Release|Any CPU.ActiveCfg = Release|Any CPU
23+
{EEB7710D-8D36-577B-90CF-7D20A60FF533}.Release|Any CPU.Build.0 = Release|Any CPU
24+
EndGlobalSection
25+
GlobalSection(SolutionProperties) = preSolution
26+
HideSolutionNode = FALSE
27+
EndGlobalSection
28+
GlobalSection(ExtensibilityGlobals) = postSolution
29+
SolutionGuid = {D254FC9D-1B6A-414B-9595-FA1DB0EBEB83}
30+
EndGlobalSection
31+
EndGlobal
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<PropertyGroup>
4+
<OutputType>Exe</OutputType>
5+
<TargetFramework>net9.0</TargetFramework>
6+
<ImplicitUsings>enable</ImplicitUsings>
7+
<Nullable>enable</Nullable>
8+
<PublishAot>true</PublishAot>
9+
<InvariantGlobalization>true</InvariantGlobalization>
10+
</PropertyGroup>
11+
12+
<ItemGroup>
13+
<PackageReference Include="Sep" Version="0.9.0" />
14+
</ItemGroup>
15+
16+
<ItemGroup>
17+
<ProjectReference Include="..\..\spectre.console\src\Spectre.Console\Spectre.Console.csproj" />
18+
</ItemGroup>
19+
20+
</Project>
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
using System.Text.Json.Serialization;
2+
3+
namespace CS2.Util.ModVerifier.Models;
4+
5+
public partial class ModInfo
6+
{
7+
[JsonPropertyName("Id")] public long Id { get; set; }
8+
9+
[JsonPropertyName("Name")] public string Name { get; set; }
10+
11+
[JsonPropertyName("DisplayName")] public string DisplayName { get; set; }
12+
13+
[JsonPropertyName("Author")] public string Author { get; set; }
14+
15+
[JsonPropertyName("ShortDescription")] public string ShortDescription { get; set; }
16+
17+
[JsonPropertyName("LongDescription")] public string LongDescription { get; set; }
18+
19+
[JsonPropertyName("RequiredGameVersion")]
20+
public string RequiredGameVersion { get; set; }
21+
22+
[JsonPropertyName("LatestVersion")] public string LatestVersion { get; set; }
23+
24+
[JsonPropertyName("Version")] public string Version { get; set; }
25+
26+
[JsonPropertyName("ThumbnailPath")] public Uri ThumbnailPath { get; set; }
27+
28+
[JsonPropertyName("Size")] public long Size { get; set; }
29+
30+
[JsonPropertyName("Tags")] public Tag[] Tags { get; set; }
31+
32+
[JsonPropertyName("Rating")] public long Rating { get; set; }
33+
34+
[JsonPropertyName("RatingsTotal")] public long RatingsTotal { get; set; }
35+
36+
[JsonPropertyName("State")] public string State { get; set; }
37+
38+
[JsonPropertyName("LocalData")] public LocalData LocalData { get; set; }
39+
40+
[JsonPropertyName("LatestUpdate")] public string LatestUpdate { get; set; }
41+
42+
[JsonPropertyName("InstalledDate")] public string InstalledDate { get; set; }
43+
44+
[JsonPropertyName("Playsets")] public Playset[] Playsets { get; set; }
45+
46+
[JsonPropertyName("HasLiked")] public bool HasLiked { get; set; }
47+
}
48+
49+
public partial class LocalData
50+
{
51+
[JsonPropertyName("LocalType")] public string LocalType { get; set; }
52+
53+
[JsonPropertyName("FolderAbsolutePath")]
54+
public string FolderAbsolutePath { get; set; }
55+
56+
[JsonPropertyName("ContentFileOrFolder")]
57+
public string ContentFileOrFolder { get; set; }
58+
59+
[JsonPropertyName("ThumbnailFilename")]
60+
public string ThumbnailFilename { get; set; }
61+
62+
[JsonPropertyName("ScreenshotsFilenames")]
63+
public string[] ScreenshotsFilenames { get; set; }
64+
}
65+
66+
public partial class Playset
67+
{
68+
[JsonPropertyName("PlaysetId")] public long PlaysetId { get; set; }
69+
70+
[JsonPropertyName("SubscribedDate")] public string SubscribedDate { get; set; }
71+
72+
[JsonPropertyName("ModIsEnabled")] public bool ModIsEnabled { get; set; }
73+
74+
[JsonPropertyName("Version")] public string Version { get; set; }
75+
76+
[JsonPropertyName("LoadOrder")] public long LoadOrder { get; set; }
77+
}
78+
79+
public partial class Tag
80+
{
81+
[JsonPropertyName("Id")] public string Id { get; set; }
82+
83+
[JsonPropertyName("DisplayName")] public string DisplayName { get; set; }
84+
}
85+
86+
[JsonSerializable(typeof(ModInfo))]
87+
public partial class ModInfoContext : JsonSerializerContext;

CS2.Util.ModVerifier/Program.cs

Lines changed: 197 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,197 @@
1+
using CS2.Util.ModVerifier.Models;
2+
using Spectre.Console;
3+
using System.Security.Cryptography;
4+
using System.Text;
5+
using System.Text.Json;
6+
using nietras.SeparatedValues;
7+
8+
namespace CS2.Util.ModVerifier
9+
{
10+
internal class Program
11+
{
12+
static async Task Main(string[] args)
13+
{
14+
Console.OutputEncoding = Encoding.UTF8;
15+
AnsiConsole.Write(new FigletText("Mod Verifier").Color(Color.LightCyan1));
16+
const string modsPath =
17+
@"%AppData%\..\LocalLow\Colossal Order\Cities Skylines II\.cache\Mods\mods_subscribed";
18+
var actualPath = Path.GetFullPath(Environment.ExpandEnvironmentVariables(modsPath));
19+
var displayPath = new TextPath(actualPath)
20+
.RootColor(Color.Red)
21+
.SeparatorColor(Color.Green)
22+
.StemColor(Color.Blue)
23+
.LeafColor(Color.Yellow);
24+
AnsiConsole.MarkupLine("[green]Current detected mods path[/]:");
25+
AnsiConsole.Write(displayPath);
26+
AnsiConsole.WriteLine();
27+
var scanMods = AnsiConsole.Prompt(
28+
new TextPrompt<bool>("Scan mods?")
29+
.AddChoice(true)
30+
.AddChoice(false)
31+
.DefaultValue(true)
32+
.WithConverter(choice => choice ? "y" : "n"));
33+
if (!scanMods)
34+
{
35+
return;
36+
}
37+
38+
var table = new Table().Centered();
39+
var modInfos = new List<ModInfo>();
40+
AnsiConsole.Live(table)
41+
.AutoClear(false)
42+
.Start(ctx =>
43+
{
44+
table.Border = TableBorder.MinimalHeavyHead;
45+
table.AddColumn("[green]Name[/]");
46+
table.AddColumn("[yellow]Id[/]");
47+
table.AddColumn("[cyan]Author[/]");
48+
table.Expand();
49+
ctx.Refresh();
50+
var mods = Directory.GetDirectories(actualPath)
51+
.Where(x => File.Exists(Path.Combine(x, ".metadata", "metadata.json")));
52+
foreach (var mod in mods)
53+
{
54+
var metadata = JsonSerializer.Deserialize(
55+
File.ReadAllText(Path.Combine(mod, ".metadata", "metadata.json")),
56+
ModInfoContext.Default.ModInfo);
57+
if (metadata is null)
58+
{
59+
ctx.Refresh();
60+
Thread.Sleep(70);
61+
continue;
62+
}
63+
64+
modInfos.Add(metadata);
65+
table.AddRow([
66+
$"[green]{metadata.DisplayName.EscapeMarkup()}[/]", $"[yellow]{metadata.Id.ToString()}[/]",
67+
$"[cyan]{metadata.Author.EscapeMarkup()}[/]"
68+
]);
69+
ctx.Refresh();
70+
Thread.Sleep(20);
71+
}
72+
});
73+
var verifyMods = AnsiConsole.Prompt(
74+
new TextPrompt<bool>("Verify mods?")
75+
.AddChoice(true)
76+
.AddChoice(false)
77+
.DefaultValue(true)
78+
.WithConverter(choice => choice ? "y" : "n"));
79+
if (!verifyMods)
80+
{
81+
return;
82+
}
83+
84+
var o = new object();
85+
var verified = new List<(ModInfo, bool)>();
86+
await AnsiConsole.Status()
87+
.StartAsync("Verifying...", async ctx =>
88+
{
89+
await Parallel.ForEachAsync(modInfos, async (mod, token) =>
90+
{
91+
var path = mod.LocalData.FolderAbsolutePath;
92+
var name = mod.Name;
93+
var manifestVersion =
94+
await File.ReadAllTextAsync(Path.Combine(path, ".cpatch", name, "version"), token);
95+
var manifests = new List<string>();
96+
await using var fs = File.OpenRead(Path.Combine(Path.Combine(path, ".cpatch", name,
97+
manifestVersion,
98+
"complete", "manifest")));
99+
using var sr = new StreamReader(fs);
100+
// Name
101+
_ = await sr.ReadLineAsync(token);
102+
// Hash method, SHA256
103+
_ = await sr.ReadLineAsync(token);
104+
// Empty
105+
_ = await sr.ReadLineAsync(token);
106+
var csvReader = await Sep.New(',').Reader(opt => opt with
107+
{
108+
HasHeader = false,
109+
DisableColCountCheck = true,
110+
Unescape = true
111+
}).FromAsync(sr, cancellationToken: token);
112+
await foreach (var results in csvReader)
113+
{
114+
var fullPath = Path.Combine(path, results[0].ToString());
115+
var file = new FileInfo(fullPath);
116+
if (!file.Exists)
117+
{
118+
lock (o)
119+
{
120+
AnsiConsole.MarkupLine(
121+
$"[red][[ERR]][/] {mod.DisplayName.EscapeMarkup()} is broken!");
122+
verified.Add((mod, false));
123+
}
124+
125+
return;
126+
}
127+
128+
if (file.Length.ToString() != results[1].ToString())
129+
{
130+
lock (o)
131+
{
132+
AnsiConsole.MarkupLine(
133+
$"[red][[ERR]][/] {mod.DisplayName.EscapeMarkup()} is broken!");
134+
verified.Add((mod, false));
135+
}
136+
137+
return;
138+
}
139+
140+
var hash = SHA256.HashData(file.OpenRead());
141+
if (Convert.ToBase64String(hash).Replace("/", "_").Replace("+", "-") ==
142+
results[2].ToString())
143+
continue;
144+
lock (o)
145+
{
146+
AnsiConsole.MarkupLine(
147+
$"[red][[ERR]][/] {mod.DisplayName.EscapeMarkup()} is broken!");
148+
verified.Add((mod, false));
149+
}
150+
151+
return;
152+
}
153+
154+
lock (o)
155+
{
156+
verified.Add((mod, true));
157+
}
158+
}
159+
);
160+
});
161+
162+
var verifiedTable = new Table().Centered();
163+
AnsiConsole.Live(verifiedTable)
164+
.AutoClear(false)
165+
.Start(ctx =>
166+
{
167+
verifiedTable.Border = TableBorder.MinimalHeavyHead;
168+
verifiedTable.AddColumn("Name");
169+
verifiedTable.AddColumn("Id");
170+
verifiedTable.AddColumn("Author");
171+
verifiedTable.AddColumn("Verified");
172+
verifiedTable.Expand();
173+
ctx.Refresh();
174+
175+
foreach (var (mod, status) in verified)
176+
{
177+
var color = status ? "green" : "red";
178+
verifiedTable.AddRow([
179+
$"[{color}]{mod.DisplayName.EscapeMarkup()}[/]",
180+
$"[{color}]{mod.Id.ToString().EscapeMarkup()}[/]",
181+
$"[{color}]{mod.Author.EscapeMarkup()}[/]",
182+
$"[{color}]{status.ToString().EscapeMarkup()}[/]"
183+
]);
184+
ctx.Refresh();
185+
Thread.Sleep(20);
186+
}
187+
});
188+
189+
AnsiConsole.Write(new BreakdownChart()
190+
.FullSize()
191+
.AddItem("Correct", verified.Count(x => x.Item2), Color.Green)
192+
.AddItem("Broken", verified.Count(x => !x.Item2), Color.Red));
193+
AnsiConsole.MarkupLine("[cyan]press any key to exit...[/]");
194+
Console.ReadKey();
195+
}
196+
}
197+
}

0 commit comments

Comments
 (0)