2 Commits

Author SHA1 Message Date
ff31412719 - add: zip content support 2026-01-16 21:02:34 +03:00
15e4e3fbd7 - fix: harmony thinks 2026-01-16 18:53:26 +03:00
11 changed files with 107 additions and 52 deletions

View File

@@ -9,21 +9,23 @@
<PackageVersion Include="AsyncImageLoader.Avalonia" Version="3.5.0" /> <PackageVersion Include="AsyncImageLoader.Avalonia" Version="3.5.0" />
<PackageVersion Include="CommunityToolkit.Mvvm" Version="8.4.0" /> <PackageVersion Include="CommunityToolkit.Mvvm" Version="8.4.0" />
<PackageVersion Include="Fluent.Net" Version="1.0.63" /> <PackageVersion Include="Fluent.Net" Version="1.0.63" />
<PackageVersion Include="JetBrains.Annotations" Version="2024.3.0" /> <PackageVersion Include="JetBrains.Annotations" Version="2025.2.4" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="10.0.2" /> <PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="10.0.2" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="10.0.2" /> <PackageVersion Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="10.0.2" />
<PackageVersion Include="libsodium" Version="1.0.20" /> <PackageVersion Include="libsodium" Version="1.0.20" />
<PackageVersion Include="Robust.Natives" Version="0.2.3" /> <PackageVersion Include="Robust.Natives" Version="0.2.3" />
<PackageVersion Include="Avalonia.Controls.ItemsRepeater" Version="11.1.5" /> <PackageVersion Include="Avalonia.Controls.ItemsRepeater" Version="11.1.5" />
<PackageVersion Include="Lib.Harmony" Version="2.3.6" /> <PackageVersion Include="Lib.Harmony" Version="2.4.2" />
<PackageVersion Include="SharpZstd.Interop" Version="1.5.6" /> <PackageVersion Include="SharpZstd.Interop" Version="1.5.6" />
<PackageVersion Include="coverlet.collector" Version="6.0.0" /> <PackageVersion Include="coverlet.collector" Version="6.0.4">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageVersion>
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.8.0" /> <PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.8.0" />
<PackageVersion Include="Moq" Version="4.20.72" /> <PackageVersion Include="Moq" Version="4.20.72" />
<PackageVersion Include="NUnit" Version="3.14.0" /> <PackageVersion Include="NUnit" Version="3.14.0" />
<PackageVersion Include="NUnit.Analyzers" Version="3.9.0" /> <PackageVersion Include="NUnit.Analyzers" Version="3.9.0" />
<PackageVersion Include="NUnit3TestAdapter" Version="4.5.0" /> <PackageVersion Include="NUnit3TestAdapter" Version="4.5.0" />
<PackageVersion Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.4" /> <PackageVersion Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.4" />
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp" Version="4.3.0" /> <PackageVersion Include="Microsoft.CodeAnalysis.CSharp" Version="4.3.0" />
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="4.3.0" /> <PackageVersion Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="4.3.0" />

View File

@@ -6,7 +6,6 @@ using Nebula.Shared;
using Nebula.Shared.Models; using Nebula.Shared.Models;
using Nebula.Shared.Services; using Nebula.Shared.Services;
using Nebula.Shared.Utils; using Nebula.Shared.Utils;
using Robust.LoaderApi;
namespace Nebula.Launcher.ProcessHelper; namespace Nebula.Launcher.ProcessHelper;
@@ -22,8 +21,7 @@ public sealed class GameRunnerPreparer(IServiceProvider provider, ContentService
if (engine is null) if (engine is null)
throw new Exception("Engine version not found: " + buildInfo.BuildInfo.Build.EngineVersion); throw new Exception("Engine version not found: " + buildInfo.BuildInfo.Build.EngineVersion);
var hashApi = await contentService.EnsureItems(buildInfo.RobustManifestInfo, loadingHandlerFactory, cancellationToken); var hashApi = await contentService.EnsureItems(buildInfo, loadingHandlerFactory, cancellationToken);
if (hashApi.TryOpen("manifest.yml", out var stream)) if (hashApi.TryOpen("manifest.yml", out var stream))
{ {

View File

@@ -57,7 +57,7 @@ public class ProcessRunHandler : IDisposable
_processInfoTask.Wait(); _processInfoTask.Wait();
} }
_process = Process.Start(_processInfo!); _process = Process.Start(_processInfo ?? throw new Exception("Process info is null, please try again."));
if (_process is null) return; if (_process is null) return;

View File

@@ -62,12 +62,11 @@ public sealed partial class DecompilerService
await stream.DisposeAsync(); await stream.DisposeAsync();
} }
var hashApi = await ContentService.EnsureItems(buildInfo, loadingHandler, cancellationToken);
var hashApi = await ContentService.EnsureItems(buildInfo.RobustManifestInfo, loadingHandler, cancellationToken); foreach (var file in hashApi.AllFiles)
foreach (var (file, hash) in hashApi.Manifest)
{ {
if(!file.Contains(".dll") || !hashApi.TryOpen(hash, out var stream)) continue; if(!file.Contains(".dll") || !hashApi.TryOpen(file, out var stream)) continue;
myTempDir.Save(Path.GetFileName(file), stream); myTempDir.Save(Path.GetFileName(file), stream);
await stream.DisposeAsync(); await stream.DisposeAsync();
} }

View File

@@ -15,11 +15,11 @@ using Nebula.Launcher.Utils;
using Nebula.Launcher.ViewModels.Popup; using Nebula.Launcher.ViewModels.Popup;
using Nebula.Launcher.Views; using Nebula.Launcher.Views;
using Nebula.Launcher.Views.Pages; using Nebula.Launcher.Views.Pages;
using Nebula.Shared.FileApis;
using Nebula.Shared.Models; using Nebula.Shared.Models;
using Nebula.Shared.Services; using Nebula.Shared.Services;
using Nebula.Shared.Utils; using Nebula.Shared.Utils;
using Nebula.Shared.ViewHelper; using Nebula.Shared.ViewHelper;
using Robust.LoaderApi;
namespace Nebula.Launcher.ViewModels.Pages; namespace Nebula.Launcher.ViewModels.Pages;
@@ -199,9 +199,9 @@ public sealed class ExtContentExecutor
_decompilerService = decompilerService; _decompilerService = decompilerService;
} }
public bool TryExecute(RobustManifestItem manifestItem, CancellationToken cancellationToken) public bool TryExecute(IFileApi api, ContentPath path, CancellationToken cancellationToken)
{ {
var ext = Path.GetExtension(manifestItem.Path); var ext = Path.GetExtension(path.GetName());
if (ext == ".dll") if (ext == ".dll")
{ {
@@ -214,42 +214,39 @@ public sealed class ExtContentExecutor
} }
public sealed partial class ManifestContentEntry : IContentEntry public sealed partial class FileContentEntry : IContentEntry
{ {
public IContentHolder Holder { get; set; } = default!; public IContentHolder Holder { get; set; } = default!;
public IContentEntry? Parent { get; set; } public IContentEntry? Parent { get; set; }
public string? Name { get; set; } public string? Name { get; set; }
public string IconPath => "/Assets/svg/file.svg"; public string IconPath => "/Assets/svg/file.svg";
private RobustManifestItem _manifestItem; private IFileApi _fileApi = default!;
private HashApi _hashApi = default!;
private ExtContentExecutor _extContentExecutor = default!; private ExtContentExecutor _extContentExecutor = default!;
public void Init(IContentHolder holder, RobustManifestItem manifestItem, HashApi api, ExtContentExecutor executor) public void Init(IContentHolder holder, IFileApi api, string fileName, ExtContentExecutor executor)
{ {
Holder = holder; Holder = holder;
Name = new ContentPath(manifestItem.Path).GetName(); Name = fileName;
_manifestItem = manifestItem; _fileApi = api;
_hashApi = api;
_extContentExecutor = executor; _extContentExecutor = executor;
} }
public IContentEntry? Go(ContentPath path, CancellationToken cancellationToken) public IContentEntry? Go(ContentPath path, CancellationToken cancellationToken)
{ {
if (_extContentExecutor.TryExecute(_manifestItem, cancellationToken)) var fullPath = ((IContentEntry)this).FullPath;
if (_extContentExecutor.TryExecute(_fileApi, fullPath, cancellationToken))
return null; return null;
var ext = Path.GetExtension(_manifestItem.Path); var ext = Path.GetExtension(fullPath.GetName());
try try
{ {
if (!_hashApi.TryOpen(_manifestItem, out var stream)) if (!_fileApi.TryOpen(fullPath.Path, out var stream))
return null; return null;
var myTempFile = Path.Combine(Path.GetTempPath(), "tempie" + ext); var myTempFile = Path.Combine(Path.GetTempPath(), "tempie" + ext);
var sw = new FileStream(myTempFile, FileMode.Create, FileAccess.Write, FileShare.None); var sw = new FileStream(myTempFile, FileMode.Create, FileAccess.Write, FileShare.None);
stream.CopyTo(sw); stream.CopyTo(sw);
@@ -298,7 +295,7 @@ public sealed partial class ServerFolderContentEntry : BaseFolderContentEntry
public RobustUrl ServerUrl { get; private set; } public RobustUrl ServerUrl { get; private set; }
public HashApi FileApi { get; private set; } = default!; public IFileApi FileApi { get; private set; } = default!;
private ExtContentExecutor _contentExecutor = default!; private ExtContentExecutor _contentExecutor = default!;
@@ -315,12 +312,12 @@ public sealed partial class ServerFolderContentEntry : BaseFolderContentEntry
Task.Run(async () => Task.Run(async () =>
{ {
var buildInfo = await ContentService.GetBuildInfo(serverUrl, CancellationService.Token); var buildInfo = await ContentService.GetBuildInfo(serverUrl, CancellationService.Token);
FileApi = await ContentService.EnsureItems(buildInfo.RobustManifestInfo, loading, FileApi = await ContentService.EnsureItems(buildInfo, loading,
CancellationService.Token); CancellationService.Token);
foreach (var (path, item) in FileApi.Manifest) foreach (var path in FileApi.AllFiles)
{ {
CreateContent(new ContentPath(path), item); CreateContent(new ContentPath(path));
} }
IsLoading = false; IsLoading = false;
@@ -328,7 +325,7 @@ public sealed partial class ServerFolderContentEntry : BaseFolderContentEntry
}); });
} }
public ManifestContentEntry CreateContent(ContentPath path, RobustManifestItem manifestItem) public FileContentEntry CreateContent(ContentPath path)
{ {
var pathDir = path.GetDirectory(); var pathDir = path.GetDirectory();
BaseFolderContentEntry parent = this; BaseFolderContentEntry parent = this;
@@ -345,8 +342,8 @@ public sealed partial class ServerFolderContentEntry : BaseFolderContentEntry
parent = folderContentEntry as BaseFolderContentEntry ?? throw new InvalidOperationException(); parent = folderContentEntry as BaseFolderContentEntry ?? throw new InvalidOperationException();
} }
var manifestContent = new ManifestContentEntry(); var manifestContent = new FileContentEntry();
manifestContent.Init(Holder, manifestItem, FileApi, _contentExecutor); manifestContent.Init(Holder, FileApi, path.GetName(), _contentExecutor);
parent.AddChild(manifestContent); parent.AddChild(manifestContent);

View File

@@ -35,14 +35,14 @@ public sealed class RunnerService(
if (engine is null) if (engine is null)
throw new Exception("Engine version not found: " + buildInfo.BuildInfo.Build.EngineVersion); throw new Exception("Engine version not found: " + buildInfo.BuildInfo.Build.EngineVersion);
var hashApi = await contentService.EnsureItems(buildInfo.RobustManifestInfo, loadingHandler, cancellationToken); var fileApi = await contentService.EnsureItems(buildInfo, loadingHandler, cancellationToken);
var extraMounts = new List<ApiMount> var extraMounts = new List<ApiMount>
{ {
new(hashApi, "/") new(fileApi, "/")
}; };
if (hashApi.TryOpen("manifest.yml", out var stream)) if (fileApi.TryOpen("manifest.yml", out var stream))
{ {
var modules = ContentManifestParser.ExtractModules(stream); var modules = ContentManifestParser.ExtractModules(stream);

View File

@@ -3,6 +3,7 @@ namespace Nebula.Shared.Models;
public class RobustBuildInfo public class RobustBuildInfo
{ {
public ServerInfo BuildInfo = default!; public ServerInfo BuildInfo = default!;
public RobustManifestInfo RobustManifestInfo; public RobustManifestInfo? RobustManifestInfo;
public RobustZipContentInfo? DownloadUri;
public RobustUrl Url = default!; public RobustUrl Url = default!;
} }

View File

@@ -1,3 +1,4 @@
namespace Nebula.Shared.Models; namespace Nebula.Shared.Models;
public record struct RobustManifestInfo(Uri ManifestUri, Uri DownloadUri, string Hash); public record struct RobustManifestInfo(Uri ManifestUri, Uri DownloadUri, string Hash);
public record struct RobustZipContentInfo(Uri DownloadUri, string Hash);

View File

@@ -20,7 +20,7 @@ public sealed record BuildInfo(
string ManifestDownloadUrl, string ManifestDownloadUrl,
[property: JsonPropertyName("manifest_url")] [property: JsonPropertyName("manifest_url")]
string ManifestUrl, string ManifestUrl,
[property: JsonPropertyName("acz")] bool Acz, [property: JsonPropertyName("acz")] bool? Acz,
[property: JsonPropertyName("hash")] string Hash, [property: JsonPropertyName("hash")] string Hash,
[property: JsonPropertyName("manifest_hash")] [property: JsonPropertyName("manifest_hash")]
string ManifestHash); string ManifestHash);

View File

@@ -1,12 +1,15 @@
using System.Buffers.Binary; using System.Buffers.Binary;
using System.Diagnostics; using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Globalization; using System.Globalization;
using System.IO.Compression;
using System.Net.Http.Headers; using System.Net.Http.Headers;
using System.Numerics; using System.Numerics;
using Nebula.Shared.FileApis; using Nebula.Shared.FileApis;
using Nebula.Shared.FileApis.Interfaces; using Nebula.Shared.FileApis.Interfaces;
using Nebula.Shared.Models; using Nebula.Shared.Models;
using Nebula.Shared.Utils; using Nebula.Shared.Utils;
using Robust.LoaderApi;
namespace Nebula.Shared.Services; namespace Nebula.Shared.Services;
@@ -14,6 +17,7 @@ public partial class ContentService
{ {
public readonly IReadWriteFileApi ContentFileApi = fileService.CreateFileApi("content"); public readonly IReadWriteFileApi ContentFileApi = fileService.CreateFileApi("content");
public readonly IReadWriteFileApi ManifestFileApi = fileService.CreateFileApi("manifest"); public readonly IReadWriteFileApi ManifestFileApi = fileService.CreateFileApi("manifest");
public readonly IReadWriteFileApi ZipContentApi = fileService.CreateFileApi("zipContent");
public void SetServerHash(string address, string hash) public void SetServerHash(string address, string hash)
{ {
@@ -34,7 +38,19 @@ public partial class ContentService
return new HashApi(manifestItems, ContentFileApi); return new HashApi(manifestItems, ContentFileApi);
} }
public async Task<HashApi> EnsureItems(ManifestReader manifestReader, Uri downloadUri, public async Task<IFileApi> EnsureItems(RobustBuildInfo info, ILoadingHandlerFactory loadingFactory,
CancellationToken cancellationToken)
{
if (info.RobustManifestInfo.HasValue)
return await EnsureItems(info.RobustManifestInfo.Value, loadingFactory, cancellationToken);
if (info.DownloadUri.HasValue)
return await EnsureItems(info.DownloadUri.Value, loadingFactory, cancellationToken);
throw new InvalidOperationException("DownloadUri is null");
}
private async Task<HashApi> EnsureItems(ManifestReader manifestReader, Uri downloadUri,
ILoadingHandlerFactory loadingFactory, ILoadingHandlerFactory loadingFactory,
CancellationToken cancellationToken) CancellationToken cancellationToken)
{ {
@@ -58,7 +74,41 @@ public partial class ContentService
return hashApi; return hashApi;
} }
public async Task<HashApi> EnsureItems(RobustManifestInfo info, ILoadingHandlerFactory loadingFactory, private async Task<ZipFileApi> EnsureItems(RobustZipContentInfo info, ILoadingHandlerFactory loadingFactory, CancellationToken cancellationToken)
{
if (TryFromFile(ZipContentApi, info.Hash, out var zipFile))
return zipFile;
var loadingHandler = loadingFactory.CreateLoadingContext(new FileLoadingFormater());
var response = await _http.GetAsync(info.DownloadUri, cancellationToken);
response.EnsureSuccessStatusCode();
loadingHandler.SetLoadingMessage("Downloading zip content");
loadingHandler.SetJobsCount(response.Content.Headers.ContentLength ?? 0);
await using var streamContent = await response.Content.ReadAsStreamAsync(cancellationToken);
ZipContentApi.Save(info.Hash, streamContent, loadingHandler);
loadingHandler.Dispose();
if (TryFromFile(ZipContentApi, info.Hash, out zipFile))
return zipFile;
ZipContentApi.Remove(info.Hash);
throw new Exception("Failed to load zip file");
}
private bool TryFromFile(IFileApi fileApi, string path, [NotNullWhen(true)] out ZipFileApi? zipFileApi)
{
zipFileApi = null;
if(!fileApi.TryOpen(path, out var zipContent))
return false;
var zip = new ZipArchive(zipContent);
zipFileApi = new ZipFileApi(zip, null);
return true;
}
private async Task<HashApi> EnsureItems(RobustManifestInfo info, ILoadingHandlerFactory loadingFactory,
CancellationToken cancellationToken) CancellationToken cancellationToken)
{ {
_logger.Log("Getting manifest: " + info.Hash); _logger.Log("Getting manifest: " + info.Hash);
@@ -90,10 +140,10 @@ public partial class ContentService
return await EnsureItems(manifestReader, info.DownloadUri, loadingFactory, cancellationToken); return await EnsureItems(manifestReader, info.DownloadUri, loadingFactory, cancellationToken);
} }
public void Unpack(HashApi hashApi, IWriteFileApi otherApi, ILoadingHandler loadingHandler) public void Unpack(IFileApi hashApi, IWriteFileApi otherApi, ILoadingHandler loadingHandler)
{ {
_logger.Log("Unpack manifest files"); _logger.Log("Unpack manifest files");
var items = hashApi.Manifest.Values.ToList(); var items = hashApi.AllFiles.ToList();
loadingHandler.AppendJob(items.Count); loadingHandler.AppendJob(items.Count);
var options = new ParallelOptions var options = new ParallelOptions
@@ -105,13 +155,13 @@ public partial class ContentService
{ {
if (hashApi.TryOpen(item, out var stream)) if (hashApi.TryOpen(item, out var stream))
{ {
_logger.Log($"Unpack {item.Hash} to: {item.Path}"); _logger.Log($"Unpack {item}");
otherApi.Save(item.Path, stream); otherApi.Save(item, stream);
stream.Close(); stream.Close();
} }
else else
{ {
_logger.Error("Error while unpacking thinks " + item.Path); _logger.Error($"Error while unpacking thinks {item}");
} }
loadingHandler.AppendResolvedJob(); loadingHandler.AppendResolvedJob();

View File

@@ -19,7 +19,14 @@ public partial class ContentService(
info.Url = url; info.Url = url;
var bi = await restService.GetAsync<ServerInfo>(url.InfoUri, cancellationToken); var bi = await restService.GetAsync<ServerInfo>(url.InfoUri, cancellationToken);
info.BuildInfo = bi; info.BuildInfo = bi;
info.RobustManifestInfo = info.BuildInfo.Build.Acz
if (info.BuildInfo.Build.Acz is null)
{
info.DownloadUri = new RobustZipContentInfo(new Uri(info.BuildInfo.Build.DownloadUrl), info.BuildInfo.Build.Hash);
return info;
}
info.RobustManifestInfo = info.BuildInfo.Build.Acz.Value
? new RobustManifestInfo(new RobustPath(info.Url, "manifest.txt"), new RobustPath(info.Url, "download"), ? new RobustManifestInfo(new RobustPath(info.Url, "manifest.txt"), new RobustPath(info.Url, "download"),
bi.Build.ManifestHash) bi.Build.ManifestHash)
: new RobustManifestInfo(new Uri(info.BuildInfo.Build.ManifestUrl), : new RobustManifestInfo(new Uri(info.BuildInfo.Build.ManifestUrl),