diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml
new file mode 100644
index 0000000..a1cb220
--- /dev/null
+++ b/.github/workflows/publish.yml
@@ -0,0 +1,32 @@
+name: Publish
+
+on:
+ workflow_dispatch:
+ # schedule:
+ # - cron: '0 10 * * *'
+
+jobs:
+ build:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Install dependencies
+ run: sudo apt-get install -y python3-paramiko python3-lxml
+ - uses: actions/checkout@v3.6.0
+ with:
+ submodules: 'recursive'
+ - name: Setup .NET Core
+ uses: actions/setup-dotnet@v3.2.0
+ with:
+ dotnet-version: 9.0.x
+ - name: Install dependencies
+ run: dotnet restore
+ - name: Package launcher files
+ run: dotnet run Nebula.Packager
+ - name: FTP Deploy Release
+ uses: SamKirkland/FTP-Deploy-Action@v4.3.5
+ with:
+ server: ${{ secrets.FTP_SERVER }}
+ username: ${{ secrets.FTP_USERNAME }}
+ password: ${{ secrets.FTP_PASSWORD }}
+ local-dir: ./release/
+ server-dir: ./release/
diff --git a/.gitignore b/.gitignore
index add57be..e31ae9f 100644
--- a/.gitignore
+++ b/.gitignore
@@ -2,4 +2,5 @@ bin/
obj/
/packages/
riderModule.iml
-/_ReSharper.Caches/
\ No newline at end of file
+/_ReSharper.Caches/
+release/
\ No newline at end of file
diff --git a/.idea/.idea.Nebula/.idea/avalonia.xml b/.idea/.idea.Nebula/.idea/avalonia.xml
index c1ea37a..47fa533 100644
--- a/.idea/.idea.Nebula/.idea/avalonia.xml
+++ b/.idea/.idea.Nebula/.idea/avalonia.xml
@@ -33,6 +33,8 @@
+
+
diff --git a/Nebula.Launcher/Nebula.Launcher.csproj b/Nebula.Launcher/Nebula.Launcher.csproj
index 4ead1a4..7aa1311 100644
--- a/Nebula.Launcher/Nebula.Launcher.csproj
+++ b/Nebula.Launcher/Nebula.Launcher.csproj
@@ -4,6 +4,7 @@
net9.0
enable
true
+ false
app.manifest
true
true
diff --git a/Nebula.Packager/Nebula.Packager.csproj b/Nebula.Packager/Nebula.Packager.csproj
new file mode 100644
index 0000000..85b4959
--- /dev/null
+++ b/Nebula.Packager/Nebula.Packager.csproj
@@ -0,0 +1,10 @@
+
+
+
+ Exe
+ net9.0
+ enable
+ enable
+
+
+
diff --git a/Nebula.Packager/Program.cs b/Nebula.Packager/Program.cs
new file mode 100644
index 0000000..772f32d
--- /dev/null
+++ b/Nebula.Packager/Program.cs
@@ -0,0 +1,74 @@
+using System.Diagnostics;
+using System.Security.Cryptography;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+
+namespace Nebula.Packager;
+public static class Program
+{
+ public static void Main(string[] args)
+ {
+ Pack("","Release");
+ }
+
+ private static void Pack(string rootPath,string configuration)
+ {
+ var processInfo = new ProcessStartInfo
+ {
+ FileName = "dotnet",
+ ArgumentList =
+ {
+ "publish",
+ Path.Combine(rootPath,"Nebula.Launcher", "Nebula.Launcher.csproj"),
+ "-c", configuration,
+ }
+ };
+
+ var process = Process.Start(processInfo)!;
+ process.WaitForExit();
+ if(process.ExitCode != 0)
+ throw new Exception($"Packager has exited with code {process.ExitCode}");
+
+ var destinationDirectory = Path.Combine(rootPath,"release");
+ var sourceDirectory = Path.Combine("Nebula.Launcher", "bin", configuration,"publish");
+
+ if (Directory.Exists(destinationDirectory))
+ {
+ Directory.Delete(destinationDirectory, true);
+ }
+
+ Directory.CreateDirectory(destinationDirectory);
+
+ HashSet entries = new HashSet();
+
+ foreach (var fileName in Directory.EnumerateFiles(sourceDirectory, "*.*", SearchOption.AllDirectories))
+ {
+ using var md5 = MD5.Create();
+ using var stream = File.OpenRead(fileName);
+
+ var hash = md5.ComputeHash(stream);
+ var hashStr = BitConverter.ToString(hash).Replace("-", "").ToLowerInvariant();
+
+ if(!File.Exists(Path.Combine(destinationDirectory, hashStr)))
+ File.Copy(fileName, Path.Combine(destinationDirectory, hashStr));
+
+ var fileNameCut = fileName.Remove(0, sourceDirectory.Length + 1);
+
+ entries.Add(new LauncherManifestEntry(hashStr, fileNameCut));
+ Console.WriteLine($"Added {hashStr} file name {fileNameCut}");
+ }
+
+ using var manifest = File.CreateText(Path.Combine(destinationDirectory, "manifest.json"));
+ manifest.AutoFlush = true;
+ manifest.Write(JsonSerializer.Serialize(new LauncherManifest(entries)));
+ }
+}
+
+public record struct LauncherManifest(
+ [property: JsonPropertyName("entries")] HashSet Entries
+);
+
+public record struct LauncherManifestEntry(
+ [property: JsonPropertyName("hash")] string Hash,
+ [property: JsonPropertyName("path")] string Path
+);
\ No newline at end of file
diff --git a/Nebula.UpdateResolver/App.axaml b/Nebula.UpdateResolver/App.axaml
new file mode 100644
index 0000000..e7cb2f2
--- /dev/null
+++ b/Nebula.UpdateResolver/App.axaml
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Nebula.UpdateResolver/App.axaml.cs b/Nebula.UpdateResolver/App.axaml.cs
new file mode 100644
index 0000000..47c2841
--- /dev/null
+++ b/Nebula.UpdateResolver/App.axaml.cs
@@ -0,0 +1,31 @@
+using Avalonia;
+using Avalonia.Controls.ApplicationLifetimes;
+using Avalonia.Markup.Xaml;
+using Microsoft.Extensions.DependencyInjection;
+using Nebula.Shared;
+
+namespace Nebula.UpdateResolver;
+
+public partial class App : Application
+{
+ public override void Initialize()
+ {
+ AvaloniaXamlLoader.Load(this);
+ }
+
+ public override void OnFrameworkInitializationCompleted()
+ {
+ var services = new ServiceCollection();
+ services.AddServices();
+ services.AddTransient();
+
+ var serviceProvider = services.BuildServiceProvider();
+
+ if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
+ {
+ desktop.MainWindow = serviceProvider.GetService();
+ }
+
+ base.OnFrameworkInitializationCompleted();
+ }
+}
\ No newline at end of file
diff --git a/Nebula.UpdateResolver/Assets/back.gif b/Nebula.UpdateResolver/Assets/back.gif
new file mode 100644
index 0000000..c61d279
Binary files /dev/null and b/Nebula.UpdateResolver/Assets/back.gif differ
diff --git a/Nebula.UpdateResolver/LauncherManifest.cs b/Nebula.UpdateResolver/LauncherManifest.cs
new file mode 100644
index 0000000..ad9711e
--- /dev/null
+++ b/Nebula.UpdateResolver/LauncherManifest.cs
@@ -0,0 +1,13 @@
+using System.Collections.Generic;
+using System.Text.Json.Serialization;
+
+namespace Nebula.UpdateResolver;
+
+public record struct LauncherManifest(
+ [property: JsonPropertyName("entries")] HashSet Entries
+);
+
+public record struct LauncherManifestEntry(
+ [property: JsonPropertyName("hash")] string Hash,
+ [property: JsonPropertyName("path")] string Path
+ );
\ No newline at end of file
diff --git a/Nebula.UpdateResolver/MainWindow.axaml b/Nebula.UpdateResolver/MainWindow.axaml
new file mode 100644
index 0000000..25e3338
--- /dev/null
+++ b/Nebula.UpdateResolver/MainWindow.axaml
@@ -0,0 +1,40 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Nebula.UpdateResolver/MainWindow.axaml.cs b/Nebula.UpdateResolver/MainWindow.axaml.cs
new file mode 100644
index 0000000..2f8504a
--- /dev/null
+++ b/Nebula.UpdateResolver/MainWindow.axaml.cs
@@ -0,0 +1,82 @@
+using System;
+using System.Collections.Generic;
+using System.Net.Http;
+using System.Threading;
+using System.Threading.Tasks;
+using Avalonia.Controls;
+using Nebula.Shared.FileApis.Interfaces;
+using Nebula.Shared.Models;
+using Nebula.Shared.Services;
+
+namespace Nebula.UpdateResolver;
+
+public partial class MainWindow : Window
+{
+ private readonly ConfigurationService _configurationService;
+ private readonly RestService _restService;
+ private readonly HttpClient _httpClient = new HttpClient();
+ public IReadWriteFileApi FileApi { get; set; }
+
+ public MainWindow(FileService fileService, ConfigurationService configurationService, RestService restService)
+ {
+ _configurationService = configurationService;
+ _restService = restService;
+ InitializeComponent();
+ FileApi = fileService.CreateFileApi("app");
+
+ }
+
+ private async Task DownloadFiles()
+ {
+ var info = await EnsureFiles();
+
+ foreach (var file in info.ToDelete)
+ {
+ FileApi.Remove(file.Path);
+ }
+
+ foreach (var file in info.ToDownload)
+ {
+ using var response = _httpClient.GetAsync(
+ _configurationService.GetConfigValue(UpdateConVars.UpdateCacheUrl)
+ + "/" + file.Hash
+ );
+ response.Result.EnsureSuccessStatusCode();
+ await using var stream = await response.Result.Content.ReadAsStreamAsync();
+ FileApi.Save(file.Path, stream);
+ }
+ }
+
+ private async Task EnsureFiles()
+ {
+ var manifest = await _restService.GetAsync(
+ _configurationService.GetConfigValue(UpdateConVars.UpdateCacheUrl)!, CancellationToken.None);
+
+ var toDownload = new HashSet();
+ var toDelete = new HashSet();
+
+ if (_configurationService.TryGetConfigValue(UpdateConVars.CurrentLauncherManifest, out var currentManifest))
+ {
+ foreach (var file in currentManifest.Entries)
+ {
+ if(!manifest.Entries.Contains(file))
+ toDelete.Add(file);
+ }
+
+ foreach (var file in manifest.Entries)
+ {
+ if(!currentManifest.Entries.Contains(file))
+ toDownload.Add(file);
+ }
+ }
+ else
+ {
+ _configurationService.SetConfigValue(UpdateConVars.CurrentLauncherManifest, manifest);
+ toDownload = manifest.Entries;
+ }
+
+ return new ManifestEnsureInfo(toDownload, toDelete);
+ }
+}
+
+public record struct ManifestEnsureInfo(HashSet ToDownload, HashSet ToDelete);
\ No newline at end of file
diff --git a/Nebula.UpdateResolver/Nebula.UpdateResolver.csproj b/Nebula.UpdateResolver/Nebula.UpdateResolver.csproj
new file mode 100644
index 0000000..8c95be5
--- /dev/null
+++ b/Nebula.UpdateResolver/Nebula.UpdateResolver.csproj
@@ -0,0 +1,31 @@
+
+
+ WinExe
+ net9.0
+ enable
+ true
+ app.manifest
+ true
+
+
+
+
+
+
+
+
+
+
+
+
+
+ None
+ All
+
+
+
+
+
+
+
+
diff --git a/Nebula.UpdateResolver/Program.cs b/Nebula.UpdateResolver/Program.cs
new file mode 100644
index 0000000..4e0205d
--- /dev/null
+++ b/Nebula.UpdateResolver/Program.cs
@@ -0,0 +1,21 @@
+using Avalonia;
+using System;
+
+namespace Nebula.UpdateResolver;
+
+class Program
+{
+ // Initialization code. Don't use any Avalonia, third-party APIs or any
+ // SynchronizationContext-reliant code before AppMain is called: things aren't initialized
+ // yet and stuff might break.
+ [STAThread]
+ public static void Main(string[] args) => BuildAvaloniaApp()
+ .StartWithClassicDesktopLifetime(args);
+
+ // Avalonia configuration, don't remove; also used by visual designer.
+ public static AppBuilder BuildAvaloniaApp()
+ => AppBuilder.Configure()
+ .UsePlatformDetect()
+ .WithInterFont()
+ .LogToTrace();
+}
\ No newline at end of file
diff --git a/Nebula.UpdateResolver/UpdateCVars.cs b/Nebula.UpdateResolver/UpdateCVars.cs
new file mode 100644
index 0000000..0709462
--- /dev/null
+++ b/Nebula.UpdateResolver/UpdateCVars.cs
@@ -0,0 +1,13 @@
+using System;
+using Nebula.Shared.Models;
+using Nebula.Shared.Services;
+
+namespace Nebula.UpdateResolver;
+
+public static class UpdateConVars
+{
+ public static readonly ConVar UpdateCacheUrl =
+ ConVarBuilder.Build("update.url",new Uri("https://cinka.ru/nebula-launcher/files/publish/release"));
+ public static readonly ConVar CurrentLauncherManifest =
+ ConVarBuilder.Build("update.manifest");
+}
\ No newline at end of file
diff --git a/Nebula.UpdateResolver/app.manifest b/Nebula.UpdateResolver/app.manifest
new file mode 100644
index 0000000..0ef1ba1
--- /dev/null
+++ b/Nebula.UpdateResolver/app.manifest
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Nebula.sln b/Nebula.sln
index d26176c..1f9ef03 100644
--- a/Nebula.sln
+++ b/Nebula.sln
@@ -10,6 +10,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Nebula.Runner", "Nebula.Run
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Nebula.SourceGenerators", "Nebula.SourceGenerators\Nebula.SourceGenerators.csproj", "{985A8F36-AFEB-4E85-8EDB-7C9DDEC698DC}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Nebula.UpdateResolver", "Nebula.UpdateResolver\Nebula.UpdateResolver.csproj", "{90CB754D-00A1-493D-A630-02BDA0AFF31A}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Nebula.Packager", "Nebula.Packager\Nebula.Packager.csproj", "{D444A5F9-4549-467F-9398-97DD6DB1E263}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -36,5 +40,13 @@ Global
{985A8F36-AFEB-4E85-8EDB-7C9DDEC698DC}.Debug|Any CPU.Build.0 = Debug|Any CPU
{985A8F36-AFEB-4E85-8EDB-7C9DDEC698DC}.Release|Any CPU.ActiveCfg = Release|Any CPU
{985A8F36-AFEB-4E85-8EDB-7C9DDEC698DC}.Release|Any CPU.Build.0 = Release|Any CPU
+ {90CB754D-00A1-493D-A630-02BDA0AFF31A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {90CB754D-00A1-493D-A630-02BDA0AFF31A}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {90CB754D-00A1-493D-A630-02BDA0AFF31A}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {90CB754D-00A1-493D-A630-02BDA0AFF31A}.Release|Any CPU.Build.0 = Release|Any CPU
+ {D444A5F9-4549-467F-9398-97DD6DB1E263}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {D444A5F9-4549-467F-9398-97DD6DB1E263}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {D444A5F9-4549-467F-9398-97DD6DB1E263}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {D444A5F9-4549-467F-9398-97DD6DB1E263}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
EndGlobal
diff --git a/Nebula.sln.DotSettings.user b/Nebula.sln.DotSettings.user
index cd39add..4fc2048 100644
--- a/Nebula.sln.DotSettings.user
+++ b/Nebula.sln.DotSettings.user
@@ -1,2 +1,3 @@
- ForceIncluded
\ No newline at end of file
+ ForceIncluded
+ ForceIncluded
\ No newline at end of file