Skip to content

Commit

Permalink
add support for Modrinth Mod Pack installation
Browse files Browse the repository at this point in the history
  • Loading branch information
laolarou726 committed May 8, 2023
1 parent 20c45ab commit 20cbb56
Show file tree
Hide file tree
Showing 9 changed files with 293 additions and 35 deletions.
35 changes: 35 additions & 0 deletions ProjBobcat/ProjBobcat/Class/Helper/FileTypeHelper.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
using System;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using ProjBobcat.Class.Model;
using SharpCompress.Archives;

namespace ProjBobcat.Class.Helper;

public static class FileTypeHelper
{
public static async Task<AssetFileType> TryDetectFileTypeAsync(string filePath)
{
if(!File.Exists(filePath)) throw new IOException("File not found.");

var extension = Path.GetExtension(filePath);

switch (extension)
{
case ".mrpack":
return AssetFileType.ModrinthModPack;
case ".zip":
{
await using var fs = File.OpenRead(filePath);
using var archive = ArchiveFactory.Open(fs);

if(archive.Entries.Any(e => e.Key.Equals("manifest.json", StringComparison.OrdinalIgnoreCase))) return AssetFileType.CurseForgeModPack;
if(archive.Entries.Any(e => e.Key.Equals("modrinth.index.json", StringComparison.OrdinalIgnoreCase))) return AssetFileType.ModrinthModPack;
break;
}
}

return AssetFileType.Unknown;
}
}
8 changes: 8 additions & 0 deletions ProjBobcat/ProjBobcat/Class/Model/AssetFileType.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
namespace ProjBobcat.Class.Model;

public enum AssetFileType
{
CurseForgeModPack,
ModrinthModPack,
Unknown
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
using System.Collections.Generic;
using System.Text.Json.Serialization;

namespace ProjBobcat.Class.Model.Modrinth;

public class ModrinthModPackFileModel
{
[JsonPropertyName("path")]
public string? Path { get; set; }

[JsonPropertyName("hashes")]
public Dictionary<string, string> Hashes { get; set; }

[JsonPropertyName("downloads")]
public string[] Downloads { get; set; }

[JsonPropertyName("fileSize")]
public long Size { get; set; }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
using System.Collections.Generic;
using System.Text.Json.Serialization;

namespace ProjBobcat.Class.Model.Modrinth;

public class ModrinthModPackIndexModel
{
[JsonPropertyName("formatVersion")]
public int FormatVersion { get; set; }

[JsonPropertyName("game")]
public string? Game { get; set; }

[JsonPropertyName("versionId")]
public string? VersionId { get; set; }

[JsonPropertyName("name")]
public string? Name { get; set; }

[JsonPropertyName("summary")]
public string? Summary { get; set; }

[JsonPropertyName("files")]
public ModrinthModPackFileModel[] Files { get; set; }

[JsonPropertyName("dependencies")]
public Dictionary<string, string> Dependencies { get; set; }
}

[JsonSerializable(typeof(ModrinthModPackIndexModel))]
partial class ModrinthModPackIndexModelContext : JsonSerializerContext
{

}
Original file line number Diff line number Diff line change
Expand Up @@ -9,17 +9,13 @@
using ProjBobcat.Class.Helper;
using ProjBobcat.Class.Model;
using ProjBobcat.Class.Model.CurseForge;
using ProjBobcat.Event;
using ProjBobcat.Interface;
using SharpCompress.Archives;

namespace ProjBobcat.DefaultComponent.Installer;
namespace ProjBobcat.DefaultComponent.Installer.ModPackInstaller;

public class CurseForgeInstaller : InstallerBase, ICurseForgeInstaller
public sealed class CurseForgeInstaller : ModPackInstallerBase, ICurseForgeInstaller
{
readonly ConcurrentBag<DownloadFile> _failedFiles = new();
int _totalDownloaded, _needToDownload;

public string ModPackPath { get; set; }
public string GameId { get; set; }

Expand All @@ -33,6 +29,10 @@ public async Task InstallTaskAsync()
InvokeStatusChangedEvent("开始安装", 0);

var manifest = await ReadManifestTask();

if(manifest == default)
throw new Exception("无法读取到 CurseForge 的 manifest 文件");

var idPath = Path.Combine(RootPath, GamePathHelper.GetGamePath(GameId));
var downloadPath = Path.Combine(Path.GetFullPath(idPath), "mods");

Expand All @@ -41,7 +41,7 @@ public async Task InstallTaskAsync()
if (!di.Exists)
di.Create();

_needToDownload = manifest.Files.Length;
NeedToDownload = manifest.Files.Length;

var urlBlock = new TransformManyBlock<IEnumerable<CurseForgeFileModel>, (long, long)>(urls =>
{
Expand All @@ -65,9 +65,9 @@ public async Task InstallTaskAsync()
urlBags.Add(downloadFile);
_totalDownloaded++;
TotalDownloaded++;
var progress = (double)_totalDownloaded / _needToDownload * 100;
var progress = (double)TotalDownloaded / NeedToDownload * 100;
InvokeStatusChangedEvent($"成功解析 MOD [{t.Item1}] 的下载地址",
progress);
Expand All @@ -84,21 +84,21 @@ public async Task InstallTaskAsync()

await actionBlock.Completion;

_totalDownloaded = 0;
TotalDownloaded = 0;
await DownloadHelper.AdvancedDownloadListFile(urlBags, new DownloadSettings
{
DownloadParts = 4,
RetryCount = 10,
Timeout = (int)TimeSpan.FromMinutes(1).TotalMilliseconds
});

if (!_failedFiles.IsEmpty)
throw new NullReferenceException("未能下载全部的 Mods");
if (!FailedFiles.IsEmpty)
throw new Exception("未能下载全部的 Mods");

using var archive = ArchiveFactory.Open(Path.GetFullPath(ModPackPath));

_totalDownloaded = 0;
_needToDownload = archive.Entries.Count();
TotalDownloaded = 0;
NeedToDownload = archive.Entries.Count();

foreach (var entry in archive.Entries)
{
Expand All @@ -124,12 +124,12 @@ public async Task InstallTaskAsync()
? $"...{subPath[(subPathLength - 15)..]}"
: subPath;

InvokeStatusChangedEvent($"解压缩安装文件:{subPathName}", (double)_totalDownloaded / _needToDownload * 100);
InvokeStatusChangedEvent($"解压缩安装文件:{subPathName}", (double)TotalDownloaded / NeedToDownload * 100);

await using var fs = File.OpenWrite(path);
entry.WriteTo(fs);

_totalDownloaded++;
TotalDownloaded++;
}

InvokeStatusChangedEvent("安装完成", 100);
Expand All @@ -150,22 +150,4 @@ public async Task InstallTaskAsync()

return manifestModel;
}

void WhenCompleted(object? sender, DownloadFileCompletedEventArgs e)
{
if (sender is not DownloadFile file) return;

_totalDownloaded++;

var progress = (double)_totalDownloaded / _needToDownload * 100;
var retryStr = file.RetryCount > 0 ? $"[重试 - {file.RetryCount}] " : string.Empty;
var fileName = file.FileName.Length > 20
? $"{file.FileName[..20]}..."
: file.FileName;

InvokeStatusChangedEvent($"{retryStr}下载整合包中的 Mods - {fileName} ({_totalDownloaded} / {_needToDownload})",
progress);

if (!(e.Success ?? false)) _failedFiles.Add(file);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
using System.Collections.Concurrent;
using ProjBobcat.Class.Model;
using ProjBobcat.Event;

namespace ProjBobcat.DefaultComponent.Installer.ModPackInstaller;

public class ModPackInstallerBase : InstallerBase
{
protected readonly ConcurrentBag<DownloadFile> FailedFiles = new();
protected int TotalDownloaded, NeedToDownload;

protected void WhenCompleted(object? sender, DownloadFileCompletedEventArgs e)
{
if (sender is not DownloadFile file) return;

TotalDownloaded++;

var progress = (double)TotalDownloaded / NeedToDownload * 100;
var retryStr = file.RetryCount > 0 ? $"[重试 - {file.RetryCount}] " : string.Empty;
var fileName = file.FileName.Length > 20
? $"{file.FileName[..20]}..."
: file.FileName;

InvokeStatusChangedEvent($"{retryStr}下载整合包中的 Mods - {fileName} ({TotalDownloaded} / {NeedToDownload})",
progress);

if (!(e.Success ?? false)) FailedFiles.Add(file);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.Json;
using System.Threading.Tasks;
using ProjBobcat.Class.Helper;
using ProjBobcat.Class.Model;
using ProjBobcat.Class.Model.Modrinth;
using ProjBobcat.Interface;
using SharpCompress.Archives;

namespace ProjBobcat.DefaultComponent.Installer.ModPackInstaller;

public sealed class ModrinthInstaller : ModPackInstallerBase, IModrinthInstaller
{
public string GameId { get; set; }
public string ModPackPath { get; set; }

public async Task<ModrinthModPackIndexModel?> ReadIndexTask()
{
using var archive = ArchiveFactory.Open(Path.GetFullPath(ModPackPath));
var manifestEntry =
archive.Entries.FirstOrDefault(x => x.Key.Equals("modrinth.index.json", StringComparison.OrdinalIgnoreCase));

if (manifestEntry == default)
return default;

await using var stream = manifestEntry.OpenEntryStream();

var manifestModel = await JsonSerializer.DeserializeAsync(stream, ModrinthModPackIndexModelContext.Default.ModrinthModPackIndexModel);

return manifestModel;
}

public void Install()
{
InstallTaskAsync().Wait();
}

public async Task InstallTaskAsync()
{
InvokeStatusChangedEvent("开始安装", 0);

var index = await ReadIndexTask();

if (index == default)
throw new Exception("无法读取到 Modrinth 的 manifest 文件");

var idPath = Path.Combine(RootPath, GamePathHelper.GetGamePath(GameId));
var downloadPath = Path.Combine(Path.GetFullPath(idPath), "mods");

var di = new DirectoryInfo(downloadPath);

if (!di.Exists)
di.Create();

var downloadFiles = new List<DownloadFile>();

foreach (var file in index.Files)
{
if (string.IsNullOrEmpty(file.Path)) continue;
if(file.Downloads.Length == 0) continue;

var fullPath = Path.Combine(idPath, file.Path);
var downloadDir = Path.GetDirectoryName(fullPath);
var fileName = Path.GetFileName(fullPath);
var checkSum = file.Hashes.TryGetValue("sha1", out var sha1) ? sha1 : string.Empty;

var df = new DownloadFile
{
CheckSum = checkSum,
DownloadPath = downloadDir,
DownloadUri = file.Downloads.RandomSample(),
FileName = fileName,
FileSize = file.Size
};
df.Completed += WhenCompleted;

downloadFiles.Add(df);
}

TotalDownloaded = 0;
NeedToDownload = downloadFiles.Count;
await DownloadHelper.AdvancedDownloadListFile(downloadFiles, new DownloadSettings
{
DownloadParts = 4,
RetryCount = 10,
Timeout = (int)TimeSpan.FromMinutes(1).TotalMilliseconds,
CheckFile = true,
HashType = HashType.SHA1
});

if (!FailedFiles.IsEmpty)
throw new NullReferenceException("未能下载全部的 Mods");

using var archive = ArchiveFactory.Open(Path.GetFullPath(ModPackPath));

TotalDownloaded = 0;
NeedToDownload = archive.Entries.Count();

const string decompressPrefix = "overrides";

foreach (var entry in archive.Entries)
{
if (!entry.Key.StartsWith(decompressPrefix, StringComparison.OrdinalIgnoreCase)) continue;

var subPath = entry.Key[(decompressPrefix.Length + 1)..];
if (string.IsNullOrEmpty(subPath)) continue;

var path = Path.Combine(Path.GetFullPath(idPath), subPath);
var dirPath = Path.GetDirectoryName(path);

if (!Directory.Exists(dirPath))
Directory.CreateDirectory(dirPath);
if (entry.IsDirectory)
{
if (!Directory.Exists(path))
Directory.CreateDirectory(path);
continue;
}

var subPathLength = subPath.Length;
var subPathName = subPathLength > 35
? $"...{subPath[(subPathLength - 15)..]}"
: subPath;

InvokeStatusChangedEvent($"解压缩安装文件:{subPathName}", (double)TotalDownloaded / NeedToDownload * 100);

await using var fs = File.OpenWrite(path);
entry.WriteTo(fs);

TotalDownloaded++;
}

InvokeStatusChangedEvent("安装完成", 100);
}
}
Loading

0 comments on commit 20cbb56

Please sign in to comment.