- add: Service think
This commit is contained in:
3
.gitmodules
vendored
Normal file
3
.gitmodules
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
[submodule "Robust.LoaderApi"]
|
||||||
|
path = Robust.LoaderApi
|
||||||
|
url = https://github.com/space-wizards/Robust.LoaderApi
|
||||||
2
.idea/.idea.Nebula/.idea/avalonia.xml
generated
2
.idea/.idea.Nebula/.idea/avalonia.xml
generated
@@ -4,6 +4,8 @@
|
|||||||
<option name="projectPerEditor">
|
<option name="projectPerEditor">
|
||||||
<map>
|
<map>
|
||||||
<entry key="Nebula.Launcher/App.axaml" value="Nebula.Launcher/Nebula.Launcher.csproj" />
|
<entry key="Nebula.Launcher/App.axaml" value="Nebula.Launcher/Nebula.Launcher.csproj" />
|
||||||
|
<entry key="Nebula.Launcher/Assets/Icons.axaml" value="Nebula.Launcher/Nebula.Launcher.csproj" />
|
||||||
|
<entry key="Nebula.Launcher/Assets/Style.axaml" value="Nebula.Launcher/Nebula.Launcher.csproj" />
|
||||||
<entry key="Nebula.Launcher/ViewModels/Styles1.axaml" value="Nebula.Launcher/Nebula.Launcher.csproj" />
|
<entry key="Nebula.Launcher/ViewModels/Styles1.axaml" value="Nebula.Launcher/Nebula.Launcher.csproj" />
|
||||||
<entry key="Nebula.Launcher/Views/Controls/PlayerContainerControl.axaml" value="Nebula.Launcher/Nebula.Launcher.csproj" />
|
<entry key="Nebula.Launcher/Views/Controls/PlayerContainerControl.axaml" value="Nebula.Launcher/Nebula.Launcher.csproj" />
|
||||||
<entry key="Nebula.Launcher/Views/Controls/ServerContainerControl.axaml" value="Nebula.Launcher/Nebula.Launcher.csproj" />
|
<entry key="Nebula.Launcher/Views/Controls/ServerContainerControl.axaml" value="Nebula.Launcher/Nebula.Launcher.csproj" />
|
||||||
|
|||||||
@@ -11,57 +11,7 @@
|
|||||||
|
|
||||||
<Application.Styles>
|
<Application.Styles>
|
||||||
<FluentTheme />
|
<FluentTheme />
|
||||||
<Style Selector="Window">
|
<StyleInclude Source="Assets/Icons.axaml" />
|
||||||
<Setter Property="Background" Value="#121212" />
|
<StyleInclude Source="Assets/Style.axaml" />
|
||||||
</Style>
|
|
||||||
<Style Selector="Border">
|
|
||||||
<Setter Property="BorderBrush" Value="#343334" />
|
|
||||||
<Setter Property="Background" Value="#222222" />
|
|
||||||
<Setter Property="BorderThickness" Value="0" />
|
|
||||||
</Style>
|
|
||||||
<Style Selector="Label">
|
|
||||||
<Setter Property="Foreground" Value="#f7f7ff" />
|
|
||||||
</Style>
|
|
||||||
|
|
||||||
<Style Selector="Button">
|
|
||||||
<Setter Property="BorderBrush" Value="#343334" />
|
|
||||||
<Setter Property="BorderThickness" Value="0" />
|
|
||||||
<Setter Property="Padding" Value="5" />
|
|
||||||
<Setter Property="CornerRadius" Value="10" />
|
|
||||||
</Style>
|
|
||||||
|
|
||||||
<Style Selector="Button:pressed">
|
|
||||||
<Setter Property="RenderTransform" Value="{x:Null}" />
|
|
||||||
</Style>
|
|
||||||
|
|
||||||
<Style Selector="Border.ButtonBack">
|
|
||||||
<Setter Property="Background" Value="#e63462" />
|
|
||||||
<Setter Property="CornerRadius" Value="0,0,10,0" />
|
|
||||||
</Style>
|
|
||||||
|
|
||||||
<Style Selector="Button.ViewSelectButton">
|
|
||||||
<Setter Property="CornerRadius" Value="0,8,8,0" />
|
|
||||||
<Setter Property="Margin" Value="0,0,0,5" />
|
|
||||||
<Setter Property="Padding" Value="8" />
|
|
||||||
<Setter Property="Background" Value="#00000000" />
|
|
||||||
</Style>
|
|
||||||
|
|
||||||
<Style Selector="Button.ViewSelectButton:pressed">
|
|
||||||
<Setter Property="BorderThickness" Value="0,0,0,0" />
|
|
||||||
</Style>
|
|
||||||
|
|
||||||
<Style Selector="TextBox">
|
|
||||||
<Setter Property="Foreground" Value="#f7f7ff" />
|
|
||||||
<Setter Property="SelectionForegroundBrush" Value="#f7f7ff" />
|
|
||||||
<Setter Property="BorderThickness" Value="0,0,0,1" />
|
|
||||||
<Setter Property="BorderBrush" Value="#f7f7ff" />
|
|
||||||
</Style>
|
|
||||||
|
|
||||||
<Style Selector="ListBoxItem">
|
|
||||||
<Setter Property="CornerRadius" Value="0,8,8,0" />
|
|
||||||
<Setter Property="Margin" Value="0,0,0,5" />
|
|
||||||
<Setter Property="Padding" Value="8" />
|
|
||||||
<Setter Property="Background" Value="#00000000" />
|
|
||||||
</Style>
|
|
||||||
</Application.Styles>
|
</Application.Styles>
|
||||||
</Application>
|
</Application>
|
||||||
18
Nebula.Launcher/Assets/Icons.axaml
Normal file
18
Nebula.Launcher/Assets/Icons.axaml
Normal file
File diff suppressed because one or more lines are too long
60
Nebula.Launcher/Assets/Style.axaml
Normal file
60
Nebula.Launcher/Assets/Style.axaml
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
<Styles xmlns="https://github.com/avaloniaui" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
|
||||||
|
<Design.PreviewWith>
|
||||||
|
<Border Padding="20">
|
||||||
|
<!-- Add Controls for Previewer Here -->
|
||||||
|
</Border>
|
||||||
|
</Design.PreviewWith>
|
||||||
|
|
||||||
|
<Style Selector="Window">
|
||||||
|
<Setter Property="Background" Value="#121212" />
|
||||||
|
</Style>
|
||||||
|
<Style Selector="Border">
|
||||||
|
<Setter Property="BorderBrush" Value="#343334" />
|
||||||
|
<Setter Property="Background" Value="#222222" />
|
||||||
|
<Setter Property="BorderThickness" Value="0" />
|
||||||
|
</Style>
|
||||||
|
<Style Selector="Label">
|
||||||
|
<Setter Property="Foreground" Value="#f7f7ff" />
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<Style Selector="Button">
|
||||||
|
<Setter Property="BorderBrush" Value="#343334" />
|
||||||
|
<Setter Property="BorderThickness" Value="0" />
|
||||||
|
<Setter Property="Padding" Value="5" />
|
||||||
|
<Setter Property="CornerRadius" Value="10" />
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<Style Selector="Button:pressed">
|
||||||
|
<Setter Property="RenderTransform" Value="{x:Null}" />
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<Style Selector="Border.ButtonBack">
|
||||||
|
<Setter Property="Background" Value="#e63462" />
|
||||||
|
<Setter Property="CornerRadius" Value="0,0,10,0" />
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<Style Selector="Button.ViewSelectButton">
|
||||||
|
<Setter Property="CornerRadius" Value="0,8,8,0" />
|
||||||
|
<Setter Property="Margin" Value="0,0,0,5" />
|
||||||
|
<Setter Property="Padding" Value="8" />
|
||||||
|
<Setter Property="Background" Value="#00000000" />
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<Style Selector="Button.ViewSelectButton:pressed">
|
||||||
|
<Setter Property="BorderThickness" Value="0,0,0,0" />
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<Style Selector="TextBox">
|
||||||
|
<Setter Property="Foreground" Value="#f7f7ff" />
|
||||||
|
<Setter Property="SelectionForegroundBrush" Value="#f7f7ff" />
|
||||||
|
<Setter Property="BorderThickness" Value="0,0,0,1" />
|
||||||
|
<Setter Property="BorderBrush" Value="#f7f7ff" />
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<Style Selector="ListBoxItem">
|
||||||
|
<Setter Property="CornerRadius" Value="0,8,8,0" />
|
||||||
|
<Setter Property="Margin" Value="0,0,0,5" />
|
||||||
|
<Setter Property="Padding" Value="8" />
|
||||||
|
<Setter Property="Background" Value="#00000000" />
|
||||||
|
</Style>
|
||||||
|
</Styles>
|
||||||
22
Nebula.Launcher/CurrentConVar.cs
Normal file
22
Nebula.Launcher/CurrentConVar.cs
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
using Nebula.Launcher.Services;
|
||||||
|
|
||||||
|
namespace Nebula.Launcher;
|
||||||
|
|
||||||
|
public static class CurrentConVar
|
||||||
|
{
|
||||||
|
public static readonly ConVar EngineManifestUrl =
|
||||||
|
ConVar.Build<string>("engine.manifestUrl", "https://robust-builds.cdn.spacestation14.com/manifest.json");
|
||||||
|
public static readonly ConVar EngineModuleManifestUrl =
|
||||||
|
ConVar.Build<string>("engine.moduleManifestUrl", "https://robust-builds.cdn.spacestation14.com/modules.json");
|
||||||
|
public static readonly ConVar ManifestDownloadProtocolVersion =
|
||||||
|
ConVar.Build<int>("engine.manifestDownloadProtocolVersion", 1);
|
||||||
|
public static readonly ConVar RobustAssemblyName =
|
||||||
|
ConVar.Build("engine.robustAssemblyName", "Robust.Client");
|
||||||
|
|
||||||
|
public static readonly ConVar Hub = ConVar.Build<string[]>("launcher.hub", [
|
||||||
|
"https://hub.spacestation14.com/api/servers"
|
||||||
|
]);
|
||||||
|
public static readonly ConVar AuthServers = ConVar.Build<string[]>("launcher.authServers", [
|
||||||
|
"https://auth.spacestation14.com/api/auth/authenticate"
|
||||||
|
]);
|
||||||
|
}
|
||||||
22
Nebula.Launcher/FileApis/AssemblyApi.cs
Normal file
22
Nebula.Launcher/FileApis/AssemblyApi.cs
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using Robust.LoaderApi;
|
||||||
|
|
||||||
|
namespace Nebula.Launcher.FileApis;
|
||||||
|
|
||||||
|
public class AssemblyApi : IFileApi
|
||||||
|
{
|
||||||
|
private readonly IFileApi _root;
|
||||||
|
|
||||||
|
public AssemblyApi(IFileApi root)
|
||||||
|
{
|
||||||
|
_root = root;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool TryOpen(string path, out Stream? stream)
|
||||||
|
{
|
||||||
|
return _root.TryOpen(path, out stream);
|
||||||
|
}
|
||||||
|
|
||||||
|
public IEnumerable<string> AllFiles => _root.AllFiles;
|
||||||
|
}
|
||||||
55
Nebula.Launcher/FileApis/FileApi.cs
Normal file
55
Nebula.Launcher/FileApis/FileApi.cs
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using Nebula.Launcher.FileApis.Interfaces;
|
||||||
|
|
||||||
|
namespace Nebula.Launcher.FileApis;
|
||||||
|
|
||||||
|
public class FileApi : IReadWriteFileApi
|
||||||
|
{
|
||||||
|
public string RootPath;
|
||||||
|
|
||||||
|
public FileApi(string rootPath)
|
||||||
|
{
|
||||||
|
RootPath = rootPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool TryOpen(string path, out Stream? stream)
|
||||||
|
{
|
||||||
|
if (File.Exists(Path.Join(RootPath, path)))
|
||||||
|
{
|
||||||
|
stream = File.OpenRead(Path.Join(RootPath, path));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
stream = null;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool Save(string path, Stream input)
|
||||||
|
{
|
||||||
|
var currPath = Path.Join(RootPath, path);
|
||||||
|
|
||||||
|
var dirInfo = new DirectoryInfo(Path.GetDirectoryName(currPath));
|
||||||
|
if (!dirInfo.Exists) dirInfo.Create();
|
||||||
|
|
||||||
|
using var stream = File.OpenWrite(currPath);
|
||||||
|
input.CopyTo(stream);
|
||||||
|
stream.Close();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool Remove(string path)
|
||||||
|
{
|
||||||
|
if (!Has(path)) return false;
|
||||||
|
File.Delete(Path.Join(RootPath, path));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool Has(string path)
|
||||||
|
{
|
||||||
|
var currPath = Path.Join(RootPath, path);
|
||||||
|
return File.Exists(currPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
public IEnumerable<string> AllFiles => Directory.EnumerateFiles(RootPath, "*.*", SearchOption.AllDirectories);
|
||||||
|
}
|
||||||
32
Nebula.Launcher/FileApis/HashApi.cs
Normal file
32
Nebula.Launcher/FileApis/HashApi.cs
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using Nebula.Launcher.Utils;
|
||||||
|
using Robust.LoaderApi;
|
||||||
|
|
||||||
|
namespace Nebula.Launcher.FileApis;
|
||||||
|
|
||||||
|
public class HashApi : IFileApi
|
||||||
|
{
|
||||||
|
private readonly IFileApi _fileApi;
|
||||||
|
public Dictionary<string, RobustManifestItem> Manifest;
|
||||||
|
|
||||||
|
public HashApi(List<RobustManifestItem> manifest, IFileApi fileApi)
|
||||||
|
{
|
||||||
|
_fileApi = fileApi;
|
||||||
|
Manifest = new Dictionary<string, RobustManifestItem>();
|
||||||
|
foreach (var item in manifest) Manifest.TryAdd(item.Path, item);
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool TryOpen(string path, out Stream? stream)
|
||||||
|
{
|
||||||
|
if (path[0] == '/') path = path.Substring(1);
|
||||||
|
|
||||||
|
if (Manifest.TryGetValue(path, out var a) && _fileApi.TryOpen(a.Hash, out stream))
|
||||||
|
return true;
|
||||||
|
|
||||||
|
stream = null;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public IEnumerable<string> AllFiles => Manifest.Keys;
|
||||||
|
}
|
||||||
7
Nebula.Launcher/FileApis/Interfaces/IReadWriteFileApi.cs
Normal file
7
Nebula.Launcher/FileApis/Interfaces/IReadWriteFileApi.cs
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
using Robust.LoaderApi;
|
||||||
|
|
||||||
|
namespace Nebula.Launcher.FileApis.Interfaces;
|
||||||
|
|
||||||
|
public interface IReadWriteFileApi : IFileApi, IWriteFileApi
|
||||||
|
{
|
||||||
|
}
|
||||||
10
Nebula.Launcher/FileApis/Interfaces/IWriteFileApi.cs
Normal file
10
Nebula.Launcher/FileApis/Interfaces/IWriteFileApi.cs
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
using System.IO;
|
||||||
|
|
||||||
|
namespace Nebula.Launcher.FileApis.Interfaces;
|
||||||
|
|
||||||
|
public interface IWriteFileApi
|
||||||
|
{
|
||||||
|
public bool Save(string path, Stream input);
|
||||||
|
public bool Remove(string path);
|
||||||
|
public bool Has(string path);
|
||||||
|
}
|
||||||
64
Nebula.Launcher/FileApis/ZipFileApi.cs
Normal file
64
Nebula.Launcher/FileApis/ZipFileApi.cs
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Diagnostics.CodeAnalysis;
|
||||||
|
using System.IO;
|
||||||
|
using System.IO.Compression;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
using Robust.LoaderApi;
|
||||||
|
|
||||||
|
namespace Nebula.Launcher.FileApis;
|
||||||
|
|
||||||
|
public sealed class ZipFileApi : IFileApi
|
||||||
|
{
|
||||||
|
private readonly ZipArchive _archive;
|
||||||
|
private readonly string? _prefix;
|
||||||
|
|
||||||
|
public ZipFileApi(ZipArchive archive, string? prefix)
|
||||||
|
{
|
||||||
|
_archive = archive;
|
||||||
|
_prefix = prefix;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool TryOpen(string path, [NotNullWhen(true)] out Stream? stream)
|
||||||
|
{
|
||||||
|
var entry = _archive.GetEntry(_prefix != null ? _prefix + path : path);
|
||||||
|
if (entry == null)
|
||||||
|
{
|
||||||
|
stream = null;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
stream = new MemoryStream();
|
||||||
|
lock (_archive)
|
||||||
|
{
|
||||||
|
using var zipStream = entry.Open();
|
||||||
|
zipStream.CopyTo(stream);
|
||||||
|
}
|
||||||
|
|
||||||
|
stream.Position = 0;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public IEnumerable<string> AllFiles
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
if (_prefix != null)
|
||||||
|
return _archive.Entries
|
||||||
|
.Where(e => e.Name != "" && e.FullName.StartsWith(_prefix))
|
||||||
|
.Select(e => e.FullName[_prefix.Length..]);
|
||||||
|
return _archive.Entries
|
||||||
|
.Where(e => e.Name != "")
|
||||||
|
.Select(entry => entry.FullName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static ZipFileApi FromPath(string path)
|
||||||
|
{
|
||||||
|
var zipArchive = new ZipArchive(File.OpenRead(path), ZipArchiveMode.Read);
|
||||||
|
|
||||||
|
var prefix = "";
|
||||||
|
if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) prefix = "Space Station 14.app/Contents/Resources/";
|
||||||
|
return new ZipFileApi(zipArchive, prefix);
|
||||||
|
}
|
||||||
|
}
|
||||||
5
Nebula.Launcher/Models/RobustManifestInfo.cs
Normal file
5
Nebula.Launcher/Models/RobustManifestInfo.cs
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
using System;
|
||||||
|
|
||||||
|
namespace Nebula.Launcher.Utils;
|
||||||
|
|
||||||
|
public record struct RobustManifestInfo(Uri ManifestUri, Uri DownloadUri, string Hash);
|
||||||
3
Nebula.Launcher/Models/RobustManifestItem.cs
Normal file
3
Nebula.Launcher/Models/RobustManifestItem.cs
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
namespace Nebula.Launcher.Utils;
|
||||||
|
|
||||||
|
public record struct RobustManifestItem(string Hash, string Path, int Id);
|
||||||
80
Nebula.Launcher/Models/RobustServerEntry.cs
Normal file
80
Nebula.Launcher/Models/RobustServerEntry.cs
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
namespace Nebula.Launcher.Models;
|
||||||
|
|
||||||
|
public sealed record Auth(string Mode, string PublicKey);
|
||||||
|
|
||||||
|
public sealed record Build(
|
||||||
|
string EngineVersion,
|
||||||
|
string ForkId,
|
||||||
|
string Version,
|
||||||
|
string DownloadUrl,
|
||||||
|
string ManifestUrl,
|
||||||
|
bool Acz,
|
||||||
|
string Hash,
|
||||||
|
string ManifestHash);
|
||||||
|
|
||||||
|
public sealed record Link(string Name, string Icon, string Url);
|
||||||
|
public sealed record Info(string ConnectAddress, Auth Auth, Build Build, string Desc, List<Link> Links);
|
||||||
|
|
||||||
|
public sealed record Status(
|
||||||
|
string Name,
|
||||||
|
int Players,
|
||||||
|
List<object> Tags,
|
||||||
|
string Map,
|
||||||
|
int RoundId,
|
||||||
|
int SoftMaxPlayer,
|
||||||
|
bool PanicBunker,
|
||||||
|
int RunLevel,
|
||||||
|
string Preset);
|
||||||
|
|
||||||
|
public enum ContentCompressionScheme
|
||||||
|
{
|
||||||
|
None = 0,
|
||||||
|
Deflate = 1,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// ZStandard compression. In the future may use SS14 specific dictionary IDs in the frame header.
|
||||||
|
/// </summary>
|
||||||
|
ZStd = 2
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed record VersionInfo(
|
||||||
|
bool Insecure,
|
||||||
|
[property: JsonPropertyName("redirect")]
|
||||||
|
string? RedirectVersion,
|
||||||
|
Dictionary<string, BuildInfo> Platforms);
|
||||||
|
|
||||||
|
public sealed class BuildInfo
|
||||||
|
{
|
||||||
|
[JsonInclude] [JsonPropertyName("sha256")]
|
||||||
|
public string Sha256 = default!;
|
||||||
|
|
||||||
|
[JsonInclude] [JsonPropertyName("sig")]
|
||||||
|
public string Signature = default!;
|
||||||
|
|
||||||
|
[JsonInclude] [JsonPropertyName("url")]
|
||||||
|
public string Url = default!;
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed record ServerInfo(string Address, StatusData StatusData, List<string> InferredTags);
|
||||||
|
|
||||||
|
public sealed record StatusData(
|
||||||
|
string Map,
|
||||||
|
string Name,
|
||||||
|
List<string> Tags,
|
||||||
|
string Preset,
|
||||||
|
int Players,
|
||||||
|
int RoundId,
|
||||||
|
int RunLevel,
|
||||||
|
bool PanicBunker,
|
||||||
|
DateTime RoundStartTime,
|
||||||
|
int SoftMaxPlayer);
|
||||||
|
|
||||||
|
public sealed record ModulesInfo(Dictionary<string, Module> Modules);
|
||||||
|
|
||||||
|
public sealed record Module(Dictionary<string, ModuleVersionInfo> Versions);
|
||||||
|
|
||||||
|
public sealed record ModuleVersionInfo(Dictionary<string, BuildInfo> Platforms);
|
||||||
@@ -37,4 +37,8 @@
|
|||||||
<SubType>Code</SubType>
|
<SubType>Code</SubType>
|
||||||
</Compile>
|
</Compile>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\Robust.LoaderApi\Robust.LoaderApi\Robust.LoaderApi.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -1,9 +1,12 @@
|
|||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Reflection;
|
||||||
using Avalonia;
|
using Avalonia;
|
||||||
using Avalonia.Controls;
|
using Avalonia.Controls;
|
||||||
using Avalonia.Controls.ApplicationLifetimes;
|
using Avalonia.Controls.ApplicationLifetimes;
|
||||||
using Avalonia.Threading;
|
using Avalonia.Threading;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Nebula.Launcher.ViewHelper;
|
||||||
using Nebula.Launcher.ViewModels;
|
using Nebula.Launcher.ViewModels;
|
||||||
using Nebula.Launcher.Views;
|
using Nebula.Launcher.Views;
|
||||||
using Nebula.Launcher.Views.Pages;
|
using Nebula.Launcher.Views.Pages;
|
||||||
@@ -38,9 +41,24 @@ public static class ServiceCollectionExtensions
|
|||||||
private static void AddViews(this IServiceCollection services)
|
private static void AddViews(this IServiceCollection services)
|
||||||
{
|
{
|
||||||
services.AddTransient<MainWindow>();
|
services.AddTransient<MainWindow>();
|
||||||
services.AddView<MainView, MainViewModel>();
|
|
||||||
services.AddView<AccountInfoView, AccountInfoViewModel>();
|
foreach (var (viewModel, view) in GetTypesWithHelpAttribute(Assembly.GetExecutingAssembly()))
|
||||||
services.AddView<ServerListView, ServerListViewModel>();
|
{
|
||||||
|
services.AddTransient(viewModel);
|
||||||
|
services.AddTransient(view);
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var (type, inference) in GetServicesWithHelpAttribute(Assembly.GetExecutingAssembly()))
|
||||||
|
{
|
||||||
|
if (inference is null)
|
||||||
|
{
|
||||||
|
services.AddSingleton(type);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
services.AddSingleton(inference, type);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void AddView<TView, TViewModel>(this IServiceCollection services)
|
private static void AddView<TView, TViewModel>(this IServiceCollection services)
|
||||||
@@ -50,4 +68,34 @@ public static class ServiceCollectionExtensions
|
|||||||
services.AddTransient<TViewModel>();
|
services.AddTransient<TViewModel>();
|
||||||
services.AddTransient<TView>();
|
services.AddTransient<TView>();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static IEnumerable<(Type,Type)> GetTypesWithHelpAttribute(Assembly assembly) {
|
||||||
|
foreach(Type type in assembly.GetTypes())
|
||||||
|
{
|
||||||
|
var attr = type.GetCustomAttribute<ViewRegisterAttribute>();
|
||||||
|
if (attr is not null) {
|
||||||
|
yield return (type, attr.Type);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static IEnumerable<(Type,Type?)> GetServicesWithHelpAttribute(Assembly assembly) {
|
||||||
|
foreach(Type type in assembly.GetTypes())
|
||||||
|
{
|
||||||
|
var attr = type.GetCustomAttribute<ServiceRegisterAttribute>();
|
||||||
|
if (attr is not null) {
|
||||||
|
yield return (type, attr.Inference);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class ServiceRegisterAttribute : Attribute
|
||||||
|
{
|
||||||
|
public Type? Inference { get; }
|
||||||
|
|
||||||
|
public ServiceRegisterAttribute(Type? inference = null)
|
||||||
|
{
|
||||||
|
Inference = inference;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
62
Nebula.Launcher/Services/ConfigurationService.cs
Normal file
62
Nebula.Launcher/Services/ConfigurationService.cs
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
using System;
|
||||||
|
using System.Diagnostics.CodeAnalysis;
|
||||||
|
|
||||||
|
namespace Nebula.Launcher.Services;
|
||||||
|
|
||||||
|
public class ConVar
|
||||||
|
{
|
||||||
|
public string Name { get; }
|
||||||
|
public Type Type { get; }
|
||||||
|
public object? DefaultValue { get; }
|
||||||
|
|
||||||
|
private ConVar(string name, Type type, object? defaultValue)
|
||||||
|
{
|
||||||
|
Name = name;
|
||||||
|
Type = type;
|
||||||
|
DefaultValue = defaultValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static ConVar Build<T>(string name, T? defaultValue = default)
|
||||||
|
{
|
||||||
|
return new ConVar(name, typeof(T), defaultValue);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[ServiceRegister]
|
||||||
|
public class ConfigurationService
|
||||||
|
{
|
||||||
|
public ConfigurationService()
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public object? GetConfigValue(ConVar conVar)
|
||||||
|
{
|
||||||
|
return conVar.DefaultValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
public T? GetConfigValue<T>(ConVar conVar)
|
||||||
|
{
|
||||||
|
var value = GetConfigValue(conVar);
|
||||||
|
if (value is not T tv) return default;
|
||||||
|
return tv;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool TryGetConfigValue(ConVar conVar,[NotNullWhen(true)] out object? value)
|
||||||
|
{
|
||||||
|
value = GetConfigValue(conVar);
|
||||||
|
return value != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool TryGetConfigValue<T>(ConVar conVar, [NotNullWhen(true)] out T? value)
|
||||||
|
{
|
||||||
|
value = GetConfigValue<T>(conVar);
|
||||||
|
return value != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SetValue(ConVar conVar, object value)
|
||||||
|
{
|
||||||
|
if(conVar.Type != value.GetType())
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
70
Nebula.Launcher/Services/DebugService.cs
Normal file
70
Nebula.Launcher/Services/DebugService.cs
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
using System;
|
||||||
|
using System.IO;
|
||||||
|
using Nebula.Launcher.Services.Logging;
|
||||||
|
|
||||||
|
namespace Nebula.Launcher.Services;
|
||||||
|
|
||||||
|
[ServiceRegister]
|
||||||
|
public class DebugService : IDisposable
|
||||||
|
{
|
||||||
|
public ILogger Logger;
|
||||||
|
|
||||||
|
private static string LogPath = Path.Combine(FileService.RootPath, "log");
|
||||||
|
public DateTime LogDate = DateTime.Now;
|
||||||
|
private FileStream LogStream;
|
||||||
|
private StreamWriter LogWriter;
|
||||||
|
|
||||||
|
public DebugService(ILogger logger)
|
||||||
|
{
|
||||||
|
Logger = logger;
|
||||||
|
|
||||||
|
if (!Directory.Exists(LogPath))
|
||||||
|
Directory.CreateDirectory(LogPath);
|
||||||
|
|
||||||
|
var filename = String.Format("{0:yyyy-MM-dd}.txt", DateTime.Now);
|
||||||
|
|
||||||
|
LogStream = File.Open(Path.Combine(LogPath, filename),
|
||||||
|
FileMode.Append, FileAccess.Write);
|
||||||
|
LogWriter = new StreamWriter(LogStream);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Debug(string message)
|
||||||
|
{
|
||||||
|
Log(LoggerCategory.Debug, message);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Error(string message)
|
||||||
|
{
|
||||||
|
Log(LoggerCategory.Error, message);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Log(string message)
|
||||||
|
{
|
||||||
|
Log(LoggerCategory.Log, message);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
LogWriter.Dispose();
|
||||||
|
LogStream.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Log(LoggerCategory category, string message)
|
||||||
|
{
|
||||||
|
Logger.Log(category, message);
|
||||||
|
SaveToLog(category, message);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SaveToLog(LoggerCategory category, string message)
|
||||||
|
{
|
||||||
|
LogWriter.WriteLine($"[{category}] {message}");
|
||||||
|
LogWriter.Flush();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum LoggerCategory
|
||||||
|
{
|
||||||
|
Log,
|
||||||
|
Debug,
|
||||||
|
Error
|
||||||
|
}
|
||||||
64
Nebula.Launcher/Services/FileService.cs
Normal file
64
Nebula.Launcher/Services/FileService.cs
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using System.IO.Compression;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
using Nebula.Launcher.FileApis;
|
||||||
|
using Nebula.Launcher.FileApis.Interfaces;
|
||||||
|
using Nebula.Launcher.Utils;
|
||||||
|
using Robust.LoaderApi;
|
||||||
|
|
||||||
|
namespace Nebula.Launcher.Services;
|
||||||
|
|
||||||
|
public class FileService
|
||||||
|
{
|
||||||
|
public static string RootPath = Path.Join(Environment.GetFolderPath(
|
||||||
|
Environment.SpecialFolder.ApplicationData), "./Datum/");
|
||||||
|
|
||||||
|
private readonly DebugService _debugService;
|
||||||
|
public readonly IReadWriteFileApi ContentFileApi;
|
||||||
|
|
||||||
|
public readonly IReadWriteFileApi EngineFileApi;
|
||||||
|
public readonly IReadWriteFileApi ManifestFileApi;
|
||||||
|
private HashApi? _hashApi;
|
||||||
|
|
||||||
|
public FileService(DebugService debugService)
|
||||||
|
{
|
||||||
|
_debugService = debugService;
|
||||||
|
ContentFileApi = CreateFileApi("content/");
|
||||||
|
EngineFileApi = CreateFileApi("engine/");
|
||||||
|
ManifestFileApi = CreateFileApi("manifest/");
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<RobustManifestItem> ManifestItems
|
||||||
|
{
|
||||||
|
set => _hashApi = new HashApi(value, ContentFileApi);
|
||||||
|
}
|
||||||
|
|
||||||
|
public HashApi HashApi
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
if (_hashApi is null) throw new Exception("Hash API is not initialized!");
|
||||||
|
return _hashApi;
|
||||||
|
}
|
||||||
|
set => _hashApi = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public IReadWriteFileApi CreateFileApi(string path)
|
||||||
|
{
|
||||||
|
return new FileApi(Path.Join(RootPath, path));
|
||||||
|
}
|
||||||
|
|
||||||
|
public ZipFileApi OpenZip(string path, IFileApi fileApi)
|
||||||
|
{
|
||||||
|
if (!fileApi.TryOpen(path, out var zipStream))
|
||||||
|
return null;
|
||||||
|
|
||||||
|
var zipArchive = new ZipArchive(zipStream, ZipArchiveMode.Read);
|
||||||
|
|
||||||
|
var prefix = "";
|
||||||
|
if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) prefix = "Space Station 14.app/Contents/Resources/";
|
||||||
|
return new ZipFileApi(zipArchive, prefix);
|
||||||
|
}
|
||||||
|
}
|
||||||
76
Nebula.Launcher/Services/HubService.cs
Normal file
76
Nebula.Launcher/Services/HubService.cs
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Collections.ObjectModel;
|
||||||
|
using System.Collections.Specialized;
|
||||||
|
using System.Threading;
|
||||||
|
using Nebula.Launcher.Models;
|
||||||
|
|
||||||
|
namespace Nebula.Launcher.Services;
|
||||||
|
|
||||||
|
[ServiceRegister]
|
||||||
|
public class HubService
|
||||||
|
{
|
||||||
|
private readonly RestService _restService;
|
||||||
|
|
||||||
|
public Action<HubServerChangedEventArgs>? HubServerChangedEventArgs;
|
||||||
|
|
||||||
|
public readonly ObservableCollection<string> HubList = new();
|
||||||
|
|
||||||
|
private readonly Dictionary<string, List<ServerInfo>> _servers = new();
|
||||||
|
|
||||||
|
|
||||||
|
public HubService(ConfigurationService configurationService, RestService restService)
|
||||||
|
{
|
||||||
|
_restService = restService;
|
||||||
|
HubList.CollectionChanged += HubListCollectionChanged;
|
||||||
|
|
||||||
|
foreach (var hubUrl in configurationService.GetConfigValue<string[]>(CurrentConVar.Hub)!)
|
||||||
|
{
|
||||||
|
HubList.Add(hubUrl);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async void HubListCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e)
|
||||||
|
{
|
||||||
|
if (e.NewItems is not null)
|
||||||
|
{
|
||||||
|
foreach (var hubUri in e.NewItems)
|
||||||
|
{
|
||||||
|
var urlStr = (string)hubUri;
|
||||||
|
var servers = await _restService.GetAsyncDefault<List<ServerInfo>>(new Uri(urlStr), [], CancellationToken.None);
|
||||||
|
_servers[urlStr] = servers;
|
||||||
|
HubServerChangedEventArgs?.Invoke(new HubServerChangedEventArgs(servers, HubServerChangeAction.Add));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (e.OldItems is not null)
|
||||||
|
{
|
||||||
|
foreach (var hubUri in e.OldItems)
|
||||||
|
{
|
||||||
|
var urlStr = (string)hubUri;
|
||||||
|
if (_servers.TryGetValue(urlStr, out var serverInfos))
|
||||||
|
{
|
||||||
|
_servers.Remove(urlStr);
|
||||||
|
HubServerChangedEventArgs?.Invoke(new HubServerChangedEventArgs(serverInfos, HubServerChangeAction.Remove));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class HubServerChangedEventArgs : EventArgs
|
||||||
|
{
|
||||||
|
public HubServerChangeAction Action;
|
||||||
|
public List<ServerInfo> Items;
|
||||||
|
|
||||||
|
public HubServerChangedEventArgs(List<ServerInfo> items, HubServerChangeAction action)
|
||||||
|
{
|
||||||
|
Items = items;
|
||||||
|
Action = action;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum HubServerChangeAction
|
||||||
|
{
|
||||||
|
Add, Remove,
|
||||||
|
}
|
||||||
15
Nebula.Launcher/Services/Logging/ConsoleLogger.cs
Normal file
15
Nebula.Launcher/Services/Logging/ConsoleLogger.cs
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
using System;
|
||||||
|
|
||||||
|
namespace Nebula.Launcher.Services.Logging;
|
||||||
|
|
||||||
|
[ServiceRegister(typeof(ILogger))]
|
||||||
|
public class ConsoleLogger : ILogger
|
||||||
|
{
|
||||||
|
public void Log(LoggerCategory loggerCategory, string message)
|
||||||
|
{
|
||||||
|
Console.ForegroundColor = ConsoleColor.DarkCyan;
|
||||||
|
Console.Write($"[{Enum.GetName(loggerCategory)}] ");
|
||||||
|
Console.ResetColor();
|
||||||
|
Console.WriteLine(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
6
Nebula.Launcher/Services/Logging/ILogger.cs
Normal file
6
Nebula.Launcher/Services/Logging/ILogger.cs
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
namespace Nebula.Launcher.Services.Logging;
|
||||||
|
|
||||||
|
public interface ILogger
|
||||||
|
{
|
||||||
|
public void Log(LoggerCategory loggerCategory, string message);
|
||||||
|
}
|
||||||
154
Nebula.Launcher/Services/RestService.cs
Normal file
154
Nebula.Launcher/Services/RestService.cs
Normal file
@@ -0,0 +1,154 @@
|
|||||||
|
using System;
|
||||||
|
using System.Globalization;
|
||||||
|
using System.IO;
|
||||||
|
using System.Net;
|
||||||
|
using System.Net.Http;
|
||||||
|
using System.Text;
|
||||||
|
using System.Text.Json;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Nebula.Launcher.Services;
|
||||||
|
|
||||||
|
[ServiceRegister]
|
||||||
|
public class RestService
|
||||||
|
{
|
||||||
|
private readonly HttpClient _client = new();
|
||||||
|
private readonly DebugService _debug;
|
||||||
|
|
||||||
|
private readonly JsonSerializerOptions _serializerOptions = new()
|
||||||
|
{
|
||||||
|
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
|
||||||
|
WriteIndented = true
|
||||||
|
};
|
||||||
|
|
||||||
|
public RestService(DebugService debug)
|
||||||
|
{
|
||||||
|
_debug = debug;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<RestResult<T>> GetAsync<T>(Uri uri, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
_debug.Debug("GET " + uri);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var response = await _client.GetAsync(uri, cancellationToken);
|
||||||
|
return await ReadResult<T>(response, cancellationToken);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_debug.Error("ERROR WHILE CONNECTION " + uri + ": " + ex.Message);
|
||||||
|
return new RestResult<T>(default, ex.Message, HttpStatusCode.RequestTimeout);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<T> GetAsyncDefault<T>(Uri uri, T defaultValue, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var result = await GetAsync<T>(uri, cancellationToken);
|
||||||
|
return result.Value ?? defaultValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<RestResult<K>> PostAsync<K, T>(T information, Uri uri, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
_debug.Debug("POST " + uri);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var json = JsonSerializer.Serialize(information, _serializerOptions);
|
||||||
|
var content = new StringContent(json, Encoding.UTF8, "application/json");
|
||||||
|
var response = await _client.PostAsync(uri, content, cancellationToken);
|
||||||
|
return await ReadResult<K>(response, cancellationToken);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_debug.Debug("ERROR " + ex.Message);
|
||||||
|
return new RestResult<K>(default, ex.Message, HttpStatusCode.RequestTimeout);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<RestResult<T>> PostAsync<T>(Stream stream, Uri uri, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
_debug.Debug("POST " + uri);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var multipartFormContent =
|
||||||
|
new MultipartFormDataContent("Upload----" + DateTime.Now.ToString(CultureInfo.InvariantCulture));
|
||||||
|
multipartFormContent.Add(new StreamContent(stream), "formFile", "image.png");
|
||||||
|
var response = await _client.PostAsync(uri, multipartFormContent, cancellationToken);
|
||||||
|
return await ReadResult<T>(response, cancellationToken);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_debug.Error("ERROR " + ex.Message);
|
||||||
|
if (ex.StackTrace != null) _debug.Error(ex.StackTrace);
|
||||||
|
return new RestResult<T>(default, ex.Message, HttpStatusCode.RequestTimeout);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<RestResult<T>> DeleteAsync<T>(Uri uri, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
_debug.Debug("DELETE " + uri);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var response = await _client.DeleteAsync(uri, cancellationToken);
|
||||||
|
return await ReadResult<T>(response, cancellationToken);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_debug.Debug("ERROR " + ex.Message);
|
||||||
|
return new RestResult<T>(default, ex.Message, HttpStatusCode.RequestTimeout);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<RestResult<T>> ReadResult<T>(HttpResponseMessage response, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var content = await response.Content.ReadAsStringAsync(cancellationToken);
|
||||||
|
//_debug.Debug("CONTENT:" + content);
|
||||||
|
|
||||||
|
if (response.IsSuccessStatusCode)
|
||||||
|
{
|
||||||
|
_debug.Debug($"SUCCESSFUL GET CONTENT {typeof(T)}");
|
||||||
|
if (typeof(T) == typeof(RawResult))
|
||||||
|
return (new RestResult<RawResult>(new RawResult(content), null, response.StatusCode) as RestResult<T>)!;
|
||||||
|
|
||||||
|
return new RestResult<T>(JsonSerializer.Deserialize<T>(content, _serializerOptions), null,
|
||||||
|
response.StatusCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
_debug.Error("ERROR " + response.StatusCode + " " + content);
|
||||||
|
return new RestResult<T>(default, "response code:" + response.StatusCode, response.StatusCode);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class RestResult<T>
|
||||||
|
{
|
||||||
|
public string? Message;
|
||||||
|
public HttpStatusCode StatusCode;
|
||||||
|
public T? Value;
|
||||||
|
|
||||||
|
public RestResult(T? value, string? message, HttpStatusCode statusCode)
|
||||||
|
{
|
||||||
|
Value = value;
|
||||||
|
Message = message;
|
||||||
|
StatusCode = statusCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static implicit operator T?(RestResult<T> result)
|
||||||
|
{
|
||||||
|
return result.Value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class RawResult
|
||||||
|
{
|
||||||
|
public string Result;
|
||||||
|
|
||||||
|
public RawResult(string result)
|
||||||
|
{
|
||||||
|
Result = result;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static implicit operator string(RawResult result)
|
||||||
|
{
|
||||||
|
return result.Result;
|
||||||
|
}
|
||||||
|
}
|
||||||
142
Nebula.Launcher/Utils/BandwidthStream.cs
Normal file
142
Nebula.Launcher/Utils/BandwidthStream.cs
Normal file
@@ -0,0 +1,142 @@
|
|||||||
|
using System;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.IO;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Nebula.Launcher.Utils;
|
||||||
|
|
||||||
|
public sealed class BandwidthStream : Stream
|
||||||
|
{
|
||||||
|
private const int NumSeconds = 8;
|
||||||
|
private const int BucketDivisor = 2;
|
||||||
|
private const int BucketsPerSecond = 2 << BucketDivisor;
|
||||||
|
|
||||||
|
// TotalBuckets MUST be power of two!
|
||||||
|
private const int TotalBuckets = NumSeconds * BucketsPerSecond;
|
||||||
|
private readonly Stream _baseStream;
|
||||||
|
private readonly long[] _buckets;
|
||||||
|
|
||||||
|
private readonly Stopwatch _stopwatch;
|
||||||
|
|
||||||
|
private long _bucketIndex;
|
||||||
|
|
||||||
|
public BandwidthStream(Stream baseStream)
|
||||||
|
{
|
||||||
|
_stopwatch = Stopwatch.StartNew();
|
||||||
|
_baseStream = baseStream;
|
||||||
|
_buckets = new long[TotalBuckets];
|
||||||
|
}
|
||||||
|
|
||||||
|
public override bool CanRead => _baseStream.CanRead;
|
||||||
|
|
||||||
|
public override bool CanSeek => _baseStream.CanSeek;
|
||||||
|
|
||||||
|
public override bool CanWrite => _baseStream.CanWrite;
|
||||||
|
|
||||||
|
public override long Length => _baseStream.Length;
|
||||||
|
|
||||||
|
public override long Position
|
||||||
|
{
|
||||||
|
get => _baseStream.Position;
|
||||||
|
set => _baseStream.Position = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void TrackBandwidth(long value)
|
||||||
|
{
|
||||||
|
const int bucketMask = TotalBuckets - 1;
|
||||||
|
|
||||||
|
var bucketIdx = CurBucketIdx();
|
||||||
|
|
||||||
|
// Increment to bucket idx, clearing along the way.
|
||||||
|
if (bucketIdx != _bucketIndex)
|
||||||
|
{
|
||||||
|
var diff = bucketIdx - _bucketIndex;
|
||||||
|
if (diff > TotalBuckets)
|
||||||
|
for (var i = _bucketIndex; i < bucketIdx; i++)
|
||||||
|
_buckets[i & bucketMask] = 0;
|
||||||
|
else
|
||||||
|
// We managed to skip so much time the whole buffer is empty.
|
||||||
|
Array.Clear(_buckets);
|
||||||
|
|
||||||
|
_bucketIndex = bucketIdx;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write value.
|
||||||
|
_buckets[bucketIdx & bucketMask] += value;
|
||||||
|
}
|
||||||
|
|
||||||
|
private long CurBucketIdx()
|
||||||
|
{
|
||||||
|
var elapsed = _stopwatch.Elapsed.TotalSeconds;
|
||||||
|
return (long)(elapsed / BucketsPerSecond);
|
||||||
|
}
|
||||||
|
|
||||||
|
public long CalcCurrentAvg()
|
||||||
|
{
|
||||||
|
var sum = 0L;
|
||||||
|
|
||||||
|
for (var i = 0; i < TotalBuckets; i++) sum += _buckets[i];
|
||||||
|
|
||||||
|
return sum >> BucketDivisor;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void Flush()
|
||||||
|
{
|
||||||
|
_baseStream.Flush();
|
||||||
|
}
|
||||||
|
|
||||||
|
public override Task FlushAsync(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
return _baseStream.FlushAsync(cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void Dispose(bool disposing)
|
||||||
|
{
|
||||||
|
if (disposing)
|
||||||
|
_baseStream.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
public override ValueTask DisposeAsync()
|
||||||
|
{
|
||||||
|
return _baseStream.DisposeAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
public override int Read(byte[] buffer, int offset, int count)
|
||||||
|
{
|
||||||
|
var read = _baseStream.Read(buffer, offset, count);
|
||||||
|
TrackBandwidth(read);
|
||||||
|
return read;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override async ValueTask<int> ReadAsync(Memory<byte> buffer, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
var read = await base.ReadAsync(buffer, cancellationToken);
|
||||||
|
TrackBandwidth(read);
|
||||||
|
return read;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override long Seek(long offset, SeekOrigin origin)
|
||||||
|
{
|
||||||
|
return _baseStream.Seek(offset, origin);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void SetLength(long value)
|
||||||
|
{
|
||||||
|
_baseStream.SetLength(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void Write(byte[] buffer, int offset, int count)
|
||||||
|
{
|
||||||
|
_baseStream.Write(buffer, offset, count);
|
||||||
|
TrackBandwidth(count);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override async ValueTask WriteAsync(
|
||||||
|
ReadOnlyMemory<byte> buffer,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
await _baseStream.WriteAsync(buffer, cancellationToken);
|
||||||
|
TrackBandwidth(buffer.Length);
|
||||||
|
}
|
||||||
|
}
|
||||||
118
Nebula.Launcher/Utils/Manifest.cs
Normal file
118
Nebula.Launcher/Utils/Manifest.cs
Normal file
@@ -0,0 +1,118 @@
|
|||||||
|
using System.Diagnostics.CodeAnalysis;
|
||||||
|
using System.IO;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
namespace Nebula.Launcher.Utils;
|
||||||
|
|
||||||
|
public class ManifestReader : StreamReader
|
||||||
|
{
|
||||||
|
public const int BufferSize = 128;
|
||||||
|
|
||||||
|
public ManifestReader(Stream stream) : base(stream)
|
||||||
|
{
|
||||||
|
ReadManifestVersion();
|
||||||
|
}
|
||||||
|
|
||||||
|
public ManifestReader(Stream stream, bool detectEncodingFromByteOrderMarks) : base(stream,
|
||||||
|
detectEncodingFromByteOrderMarks)
|
||||||
|
{
|
||||||
|
ReadManifestVersion();
|
||||||
|
}
|
||||||
|
|
||||||
|
public ManifestReader(Stream stream, Encoding encoding) : base(stream, encoding)
|
||||||
|
{
|
||||||
|
ReadManifestVersion();
|
||||||
|
}
|
||||||
|
|
||||||
|
public ManifestReader(Stream stream, Encoding encoding, bool detectEncodingFromByteOrderMarks) : base(stream,
|
||||||
|
encoding, detectEncodingFromByteOrderMarks)
|
||||||
|
{
|
||||||
|
ReadManifestVersion();
|
||||||
|
}
|
||||||
|
|
||||||
|
public ManifestReader(Stream stream, Encoding encoding, bool detectEncodingFromByteOrderMarks, int bufferSize) :
|
||||||
|
base(stream, encoding, detectEncodingFromByteOrderMarks, bufferSize)
|
||||||
|
{
|
||||||
|
ReadManifestVersion();
|
||||||
|
}
|
||||||
|
|
||||||
|
public ManifestReader(Stream stream, Encoding? encoding = null, bool detectEncodingFromByteOrderMarks = true,
|
||||||
|
int bufferSize = -1, bool leaveOpen = false) : base(stream, encoding, detectEncodingFromByteOrderMarks,
|
||||||
|
bufferSize, leaveOpen)
|
||||||
|
{
|
||||||
|
ReadManifestVersion();
|
||||||
|
}
|
||||||
|
|
||||||
|
public ManifestReader(string path) : base(path)
|
||||||
|
{
|
||||||
|
ReadManifestVersion();
|
||||||
|
}
|
||||||
|
|
||||||
|
public ManifestReader(string path, bool detectEncodingFromByteOrderMarks) : base(path,
|
||||||
|
detectEncodingFromByteOrderMarks)
|
||||||
|
{
|
||||||
|
ReadManifestVersion();
|
||||||
|
}
|
||||||
|
|
||||||
|
public ManifestReader(string path, FileStreamOptions options) : base(path, options)
|
||||||
|
{
|
||||||
|
ReadManifestVersion();
|
||||||
|
}
|
||||||
|
|
||||||
|
public ManifestReader(string path, Encoding encoding) : base(path, encoding)
|
||||||
|
{
|
||||||
|
ReadManifestVersion();
|
||||||
|
}
|
||||||
|
|
||||||
|
public ManifestReader(string path, Encoding encoding, bool detectEncodingFromByteOrderMarks) : base(path, encoding,
|
||||||
|
detectEncodingFromByteOrderMarks)
|
||||||
|
{
|
||||||
|
ReadManifestVersion();
|
||||||
|
}
|
||||||
|
|
||||||
|
public ManifestReader(string path, Encoding encoding, bool detectEncodingFromByteOrderMarks, int bufferSize) : base(
|
||||||
|
path, encoding, detectEncodingFromByteOrderMarks, bufferSize)
|
||||||
|
{
|
||||||
|
ReadManifestVersion();
|
||||||
|
}
|
||||||
|
|
||||||
|
public ManifestReader(string path, Encoding encoding, bool detectEncodingFromByteOrderMarks,
|
||||||
|
FileStreamOptions options) : base(path, encoding, detectEncodingFromByteOrderMarks, options)
|
||||||
|
{
|
||||||
|
ReadManifestVersion();
|
||||||
|
}
|
||||||
|
|
||||||
|
public string ManifestVersion { get; private set; } = "";
|
||||||
|
public int CurrentId { get; private set; }
|
||||||
|
|
||||||
|
private void ReadManifestVersion()
|
||||||
|
{
|
||||||
|
ManifestVersion = ReadLine();
|
||||||
|
}
|
||||||
|
|
||||||
|
public RobustManifestItem? ReadItem()
|
||||||
|
{
|
||||||
|
var line = ReadLine();
|
||||||
|
if (line == null) return null;
|
||||||
|
var splited = line.Split(" ");
|
||||||
|
return new RobustManifestItem(splited[0], line.Substring(splited[0].Length + 1), CurrentId++);
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool TryReadItem([NotNullWhen(true)] out RobustManifestItem? item)
|
||||||
|
{
|
||||||
|
item = ReadItem();
|
||||||
|
return item != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void Dispose(bool disposing)
|
||||||
|
{
|
||||||
|
base.Dispose(disposing);
|
||||||
|
CurrentId = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public new void DiscardBufferedData()
|
||||||
|
{
|
||||||
|
base.DiscardBufferedData();
|
||||||
|
CurrentId = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
121
Nebula.Launcher/Utils/RidUtility.cs
Normal file
121
Nebula.Launcher/Utils/RidUtility.cs
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
using System.Text.Json;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
namespace Nebula.Launcher.Utils;
|
||||||
|
|
||||||
|
public static class RidUtility
|
||||||
|
{
|
||||||
|
public static string? FindBestRid(ICollection<string> runtimes, string? currentRid = null)
|
||||||
|
{
|
||||||
|
var catalog = LoadRidCatalog();
|
||||||
|
|
||||||
|
if (currentRid == null)
|
||||||
|
{
|
||||||
|
var reportedRid = RuntimeInformation.RuntimeIdentifier;
|
||||||
|
if (!catalog.Runtimes.ContainsKey(reportedRid))
|
||||||
|
{
|
||||||
|
currentRid = GuessRid();
|
||||||
|
Console.WriteLine(".NET reported unknown RID: {0}, guessing: {1}", reportedRid, currentRid);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
currentRid = reportedRid;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Breadth-first search.
|
||||||
|
var q = new Queue<string>();
|
||||||
|
if (!catalog.Runtimes.TryGetValue(currentRid, out var root))
|
||||||
|
// RID doesn't exist in catalog???
|
||||||
|
return null;
|
||||||
|
|
||||||
|
root.Discovered = true;
|
||||||
|
q.Enqueue(currentRid);
|
||||||
|
|
||||||
|
while (q.TryDequeue(out var v))
|
||||||
|
{
|
||||||
|
if (runtimes.Contains(v)) return v;
|
||||||
|
|
||||||
|
foreach (var w in catalog.Runtimes[v].Imports)
|
||||||
|
{
|
||||||
|
var r = catalog.Runtimes[w];
|
||||||
|
if (!r.Discovered)
|
||||||
|
{
|
||||||
|
q.Enqueue(w);
|
||||||
|
r.Discovered = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string GuessRid()
|
||||||
|
{
|
||||||
|
if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
|
||||||
|
return RuntimeInformation.ProcessArchitecture switch
|
||||||
|
{
|
||||||
|
Architecture.X86 => "linux-x86",
|
||||||
|
Architecture.X64 => "linux-x64",
|
||||||
|
Architecture.Arm => "linux-arm",
|
||||||
|
Architecture.Arm64 => "linux-arm64",
|
||||||
|
_ => "unknown"
|
||||||
|
};
|
||||||
|
|
||||||
|
if (RuntimeInformation.IsOSPlatform(OSPlatform.FreeBSD))
|
||||||
|
return RuntimeInformation.ProcessArchitecture switch
|
||||||
|
{
|
||||||
|
Architecture.X64 => "freebsd-x64",
|
||||||
|
_ => "unknown"
|
||||||
|
};
|
||||||
|
|
||||||
|
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
||||||
|
return RuntimeInformation.ProcessArchitecture switch
|
||||||
|
{
|
||||||
|
Architecture.X86 => "win-x86",
|
||||||
|
Architecture.X64 => "win-x64",
|
||||||
|
Architecture.Arm => "win-arm",
|
||||||
|
Architecture.Arm64 => "win-arm64",
|
||||||
|
_ => "unknown"
|
||||||
|
};
|
||||||
|
|
||||||
|
if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
|
||||||
|
return RuntimeInformation.ProcessArchitecture switch
|
||||||
|
{
|
||||||
|
Architecture.X64 => "osx-x64",
|
||||||
|
Architecture.Arm64 => "osx-arm64",
|
||||||
|
_ => "unknown"
|
||||||
|
};
|
||||||
|
|
||||||
|
return "unknown";
|
||||||
|
}
|
||||||
|
|
||||||
|
private static RidCatalog LoadRidCatalog()
|
||||||
|
{
|
||||||
|
using var stream = typeof(RidCatalog).Assembly.GetManifestResourceStream("Utility.runtime.json")!;
|
||||||
|
var ms = new MemoryStream();
|
||||||
|
stream.CopyTo(ms);
|
||||||
|
|
||||||
|
return JsonSerializer.Deserialize<RidCatalog>(ms.GetBuffer().AsSpan(0, (int)ms.Length))!;
|
||||||
|
}
|
||||||
|
|
||||||
|
#pragma warning disable 649
|
||||||
|
private sealed class RidCatalog
|
||||||
|
{
|
||||||
|
[JsonInclude] [JsonPropertyName("runtimes")]
|
||||||
|
public Dictionary<string, Runtime> Runtimes = default!;
|
||||||
|
|
||||||
|
public class Runtime
|
||||||
|
{
|
||||||
|
public bool Discovered;
|
||||||
|
|
||||||
|
[JsonInclude] [JsonPropertyName("#import")]
|
||||||
|
public string[] Imports = default!;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#pragma warning restore 649
|
||||||
|
}
|
||||||
58
Nebula.Launcher/Utils/StreamHelper.cs
Normal file
58
Nebula.Launcher/Utils/StreamHelper.cs
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
using System;
|
||||||
|
using System.Buffers;
|
||||||
|
using System.IO;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Nebula.Launcher.Utils;
|
||||||
|
|
||||||
|
public static class StreamHelper
|
||||||
|
{
|
||||||
|
public static async ValueTask<byte[]> ReadExactAsync(this Stream stream, int amount, CancellationToken? cancel)
|
||||||
|
{
|
||||||
|
var data = new byte[amount];
|
||||||
|
await ReadExactAsync(stream, data, cancel);
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async ValueTask ReadExactAsync(this Stream stream, Memory<byte> into, CancellationToken? cancel)
|
||||||
|
{
|
||||||
|
while (into.Length > 0)
|
||||||
|
{
|
||||||
|
var read = await stream.ReadAsync(into);
|
||||||
|
|
||||||
|
// Check EOF.
|
||||||
|
if (read == 0)
|
||||||
|
throw new EndOfStreamException();
|
||||||
|
|
||||||
|
into = into[read..];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async Task CopyAmountToAsync(
|
||||||
|
this Stream stream,
|
||||||
|
Stream to,
|
||||||
|
int amount,
|
||||||
|
int bufferSize,
|
||||||
|
CancellationToken cancel)
|
||||||
|
{
|
||||||
|
var buffer = ArrayPool<byte>.Shared.Rent(bufferSize);
|
||||||
|
|
||||||
|
while (amount > 0)
|
||||||
|
{
|
||||||
|
Memory<byte> readInto = buffer;
|
||||||
|
if (amount < readInto.Length)
|
||||||
|
readInto = readInto[..amount];
|
||||||
|
|
||||||
|
var read = await stream.ReadAsync(readInto, cancel);
|
||||||
|
if (read == 0)
|
||||||
|
throw new EndOfStreamException();
|
||||||
|
|
||||||
|
amount -= read;
|
||||||
|
|
||||||
|
readInto = readInto[..read];
|
||||||
|
|
||||||
|
await to.WriteAsync(readInto, cancel);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
135
Nebula.Launcher/Utils/UriHelper.cs
Normal file
135
Nebula.Launcher/Utils/UriHelper.cs
Normal file
@@ -0,0 +1,135 @@
|
|||||||
|
using System;
|
||||||
|
using System.Diagnostics.CodeAnalysis;
|
||||||
|
using System.Diagnostics.Contracts;
|
||||||
|
using System.Web;
|
||||||
|
|
||||||
|
namespace Nebula.Launcher.Utils;
|
||||||
|
|
||||||
|
public static class UriHelper
|
||||||
|
{
|
||||||
|
public const string SchemeSs14 = "ss14";
|
||||||
|
|
||||||
|
// ReSharper disable once InconsistentNaming
|
||||||
|
public const string SchemeSs14s = "ss14s";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Parses an <c>ss14://</c> or <c>ss14s://</c> URI,
|
||||||
|
/// defaulting to <c>ss14://</c> if no scheme is specified.
|
||||||
|
/// </summary>
|
||||||
|
[Pure]
|
||||||
|
public static Uri ParseSs14Uri(string address)
|
||||||
|
{
|
||||||
|
if (!TryParseSs14Uri(address, out var uri)) throw new FormatException("Not a valid SS14 URI");
|
||||||
|
|
||||||
|
return uri;
|
||||||
|
}
|
||||||
|
|
||||||
|
[Pure]
|
||||||
|
public static bool TryParseSs14Uri(string address, [NotNullWhen(true)] out Uri? uri)
|
||||||
|
{
|
||||||
|
if (!address.Contains("://")) address = "ss14://" + address;
|
||||||
|
|
||||||
|
if (!Uri.TryCreate(address, UriKind.Absolute, out uri)) return false;
|
||||||
|
|
||||||
|
if (uri.Scheme != SchemeSs14 && uri.Scheme != SchemeSs14s) return false;
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(uri.Host))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the <c>http://</c> or <c>https://</c> API address for a server address.
|
||||||
|
/// </summary>
|
||||||
|
[Pure]
|
||||||
|
public static Uri GetServerApiAddress(Uri serverAddress)
|
||||||
|
{
|
||||||
|
var dataScheme = serverAddress.Scheme switch
|
||||||
|
{
|
||||||
|
"ss14" => Uri.UriSchemeHttp,
|
||||||
|
"ss14s" => Uri.UriSchemeHttps,
|
||||||
|
_ => throw new ArgumentException($"Wrong URI scheme: {serverAddress.Scheme}")
|
||||||
|
};
|
||||||
|
|
||||||
|
var builder = new UriBuilder(serverAddress)
|
||||||
|
{
|
||||||
|
Scheme = dataScheme
|
||||||
|
};
|
||||||
|
|
||||||
|
// No port specified.
|
||||||
|
// Default port for ss14:// is 1212, for ss14s:// it's 443 (HTTPS)
|
||||||
|
if (serverAddress.IsDefaultPort && serverAddress.Scheme == SchemeSs14) builder.Port = 1212;
|
||||||
|
|
||||||
|
if (!builder.Path.EndsWith('/')) builder.Path += "/";
|
||||||
|
|
||||||
|
return builder.Uri;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the <c>/status</c> HTTP address for a server address.
|
||||||
|
/// </summary>
|
||||||
|
[Pure]
|
||||||
|
public static Uri GetServerStatusAddress(string serverAddress)
|
||||||
|
{
|
||||||
|
return GetServerStatusAddress(ParseSs14Uri(serverAddress));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the <c>/status</c> HTTP address for an ss14 uri.
|
||||||
|
/// </summary>
|
||||||
|
[Pure]
|
||||||
|
public static Uri GetServerStatusAddress(Uri serverAddress)
|
||||||
|
{
|
||||||
|
return new Uri(GetServerApiAddress(serverAddress), "status");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the <c>/info</c> HTTP address for a server address.
|
||||||
|
/// </summary>
|
||||||
|
[Pure]
|
||||||
|
public static Uri GetServerInfoAddress(string serverAddress)
|
||||||
|
{
|
||||||
|
return GetServerInfoAddress(ParseSs14Uri(serverAddress));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the <c>/info</c> HTTP address for an ss14 uri.
|
||||||
|
/// </summary>
|
||||||
|
[Pure]
|
||||||
|
public static Uri GetServerInfoAddress(Uri serverAddress)
|
||||||
|
{
|
||||||
|
return new Uri(GetServerApiAddress(serverAddress), "info");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the <c>/client.zip</c> HTTP address for a server address.
|
||||||
|
/// This is not necessarily the actual client ZIP address.
|
||||||
|
/// </summary>
|
||||||
|
[Pure]
|
||||||
|
public static Uri GetServerSelfhostedClientZipAddress(string serverAddress)
|
||||||
|
{
|
||||||
|
return GetServerSelfhostedClientZipAddress(ParseSs14Uri(serverAddress));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the <c>/client.zip</c> HTTP address for an ss14 uri.
|
||||||
|
/// This is not necessarily the actual client ZIP address.
|
||||||
|
/// </summary>
|
||||||
|
[Pure]
|
||||||
|
public static Uri GetServerSelfhostedClientZipAddress(Uri serverAddress)
|
||||||
|
{
|
||||||
|
return new Uri(GetServerApiAddress(serverAddress), "client.zip");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Pure]
|
||||||
|
public static Uri AddParameter(this Uri url, string paramName, string paramValue)
|
||||||
|
{
|
||||||
|
var uriBuilder = new UriBuilder(url);
|
||||||
|
var query = HttpUtility.ParseQueryString(uriBuilder.Query);
|
||||||
|
query[paramName] = paramValue;
|
||||||
|
uriBuilder.Query = query.ToString();
|
||||||
|
|
||||||
|
return uriBuilder.Uri;
|
||||||
|
}
|
||||||
|
}
|
||||||
3084
Nebula.Launcher/Utils/runtime.json
Normal file
3084
Nebula.Launcher/Utils/runtime.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -36,8 +36,8 @@ public partial class MainViewModel : ViewModelBase
|
|||||||
|
|
||||||
private readonly List<ListItemTemplate> _templates =
|
private readonly List<ListItemTemplate> _templates =
|
||||||
[
|
[
|
||||||
new ListItemTemplate(typeof(AccountInfoViewModel), "HomeRegular", "Account"),
|
new ListItemTemplate(typeof(AccountInfoViewModel), "Account", "Account"),
|
||||||
new ListItemTemplate(typeof(ServerListViewModel), "List", "Servers")
|
new ListItemTemplate(typeof(ServerListViewModel), "HomeRegular", "Servers")
|
||||||
];
|
];
|
||||||
|
|
||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
@@ -51,12 +51,10 @@ public partial class MainViewModel : ViewModelBase
|
|||||||
|
|
||||||
partial void OnSelectedListItemChanged(ListItemTemplate? value)
|
partial void OnSelectedListItemChanged(ListItemTemplate? value)
|
||||||
{
|
{
|
||||||
Console.WriteLine("FUCKED " + value?.ModelType);
|
|
||||||
if (value is null) return;
|
if (value is null) return;
|
||||||
|
|
||||||
if(!TryGetViewModel(value.ModelType, out var vmb))
|
if(!TryGetViewModel(value.ModelType, out var vmb))
|
||||||
{
|
{
|
||||||
Console.WriteLine("FUCKCCC");
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,13 +1,46 @@
|
|||||||
using System;
|
using System;
|
||||||
|
using System.Collections.ObjectModel;
|
||||||
|
using CommunityToolkit.Mvvm.ComponentModel;
|
||||||
|
using Nebula.Launcher.Models;
|
||||||
|
using Nebula.Launcher.Services;
|
||||||
using Nebula.Launcher.ViewHelper;
|
using Nebula.Launcher.ViewHelper;
|
||||||
using Nebula.Launcher.Views.Pages;
|
using Nebula.Launcher.Views.Pages;
|
||||||
|
|
||||||
namespace Nebula.Launcher.ViewModels;
|
namespace Nebula.Launcher.ViewModels;
|
||||||
|
|
||||||
[ViewRegister(typeof(ServerListView))]
|
[ViewRegister(typeof(ServerListView))]
|
||||||
public class ServerListViewModel : ViewModelBase
|
public partial class ServerListViewModel : ViewModelBase
|
||||||
{
|
{
|
||||||
public ServerListViewModel(IServiceProvider serviceProvider) : base(serviceProvider)
|
public ObservableCollection<ServerInfo> ServerInfos { get; }
|
||||||
|
|
||||||
|
[ObservableProperty]
|
||||||
|
private ServerInfo? _selectedListItem;
|
||||||
|
|
||||||
|
public ServerListViewModel()
|
||||||
{
|
{
|
||||||
|
ServerInfos = new ObservableCollection<ServerInfo>();
|
||||||
|
}
|
||||||
|
public ServerListViewModel(IServiceProvider serviceProvider, HubService hubService) : base(serviceProvider)
|
||||||
|
{
|
||||||
|
ServerInfos = new ObservableCollection<ServerInfo>();
|
||||||
|
hubService.HubServerChangedEventArgs += HubServerChangedEventArgs;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void HubServerChangedEventArgs(HubServerChangedEventArgs obj)
|
||||||
|
{
|
||||||
|
if (obj.Action == HubServerChangeAction.Add)
|
||||||
|
{
|
||||||
|
foreach (var info in obj.Items)
|
||||||
|
{
|
||||||
|
ServerInfos.Add(info);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
foreach (var info in obj.Items)
|
||||||
|
{
|
||||||
|
ServerInfos.Remove(info);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -25,8 +25,6 @@ public abstract class ViewModelBase : ObservableObject
|
|||||||
? Activator.CreateInstance(type)
|
? Activator.CreateInstance(type)
|
||||||
: _serviceProvider.GetService(type);
|
: _serviceProvider.GetService(type);
|
||||||
|
|
||||||
Console.WriteLine(vm?.ToString());
|
|
||||||
|
|
||||||
if (vm is not ViewModelBase vmb) return false;
|
if (vm is not ViewModelBase vmb) return false;
|
||||||
|
|
||||||
viewModelBase = vmb;
|
viewModelBase = vmb;
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
Grid.Column="0"
|
Grid.Column="0"
|
||||||
Grid.Row="0"
|
Grid.Row="0"
|
||||||
Padding="10">
|
Padding="10">
|
||||||
<Label>Server name</Label>
|
<TextBlock x:Name="ServerNameLabel">Server name</TextBlock>
|
||||||
</Border>
|
</Border>
|
||||||
<Border
|
<Border
|
||||||
BorderThickness="2,0,0,0"
|
BorderThickness="2,0,0,0"
|
||||||
|
|||||||
@@ -1,9 +1,23 @@
|
|||||||
|
using Avalonia;
|
||||||
using Avalonia.Controls;
|
using Avalonia.Controls;
|
||||||
|
|
||||||
namespace Nebula.Launcher.Views.Controls;
|
namespace Nebula.Launcher.Views.Controls;
|
||||||
|
|
||||||
public partial class ServerContainerControl : UserControl
|
public partial class ServerContainerControl : UserControl
|
||||||
{
|
{
|
||||||
|
public static readonly StyledProperty<string> ServerNameProperty
|
||||||
|
= AvaloniaProperty.Register<ServerContainerControl, string>(nameof (ServerName));
|
||||||
|
|
||||||
|
public string ServerName
|
||||||
|
{
|
||||||
|
get => GetValue(ServerNameProperty);
|
||||||
|
set
|
||||||
|
{
|
||||||
|
SetValue(ServerNameProperty, value);
|
||||||
|
ServerNameLabel.Text = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public ServerContainerControl()
|
public ServerContainerControl()
|
||||||
{
|
{
|
||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
|
|||||||
@@ -3,28 +3,33 @@
|
|||||||
d:DesignWidth="800"
|
d:DesignWidth="800"
|
||||||
mc:Ignorable="d"
|
mc:Ignorable="d"
|
||||||
x:Class="Nebula.Launcher.Views.Pages.ServerListView"
|
x:Class="Nebula.Launcher.Views.Pages.ServerListView"
|
||||||
x:DataType="pages:ServerListView"
|
x:DataType="viewModels:ServerListViewModel"
|
||||||
xmlns="https://github.com/avaloniaui"
|
xmlns="https://github.com/avaloniaui"
|
||||||
xmlns:controls="clr-namespace:Nebula.Launcher.Views.Controls"
|
xmlns:controls="clr-namespace:Nebula.Launcher.Views.Controls"
|
||||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
xmlns:models="clr-namespace:Nebula.Launcher.Models"
|
||||||
xmlns:pages="clr-namespace:Nebula.Launcher.Views.Pages">
|
xmlns:viewModels="clr-namespace:Nebula.Launcher.ViewModels"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
|
||||||
|
|
||||||
|
<Design.DataContext>
|
||||||
|
<viewModels:ServerListViewModel />
|
||||||
|
</Design.DataContext>
|
||||||
|
|
||||||
<Grid ColumnDefinitions="*" RowDefinitions="*,40">
|
<Grid ColumnDefinitions="*" RowDefinitions="*,40">
|
||||||
<ScrollViewer Margin="0,0,0,10" Padding="0,0,8,0">
|
<ScrollViewer Margin="0,0,0,10" Padding="0,0,8,0">
|
||||||
<StackPanel>
|
<ListBox
|
||||||
<controls:ServerContainerControl />
|
Background="#00000000"
|
||||||
<controls:ServerContainerControl />
|
ItemsSource="{Binding ServerInfos}"
|
||||||
<controls:ServerContainerControl />
|
Padding="0">
|
||||||
<controls:ServerContainerControl />
|
<ListBox.ItemTemplate>
|
||||||
<controls:ServerContainerControl />
|
<DataTemplate DataType="{x:Type models:ServerInfo}">
|
||||||
<controls:ServerContainerControl />
|
<controls:ServerContainerControl ServerName="{Binding StatusData.Name}" />
|
||||||
<controls:ServerContainerControl />
|
</DataTemplate>
|
||||||
<controls:ServerContainerControl />
|
</ListBox.ItemTemplate>
|
||||||
<controls:ServerContainerControl />
|
</ListBox>
|
||||||
<controls:ServerContainerControl />
|
|
||||||
</StackPanel>
|
|
||||||
</ScrollViewer>
|
</ScrollViewer>
|
||||||
|
|
||||||
<Border
|
<Border
|
||||||
BorderThickness="2,0,0,0"
|
BorderThickness="2,0,0,0"
|
||||||
CornerRadius="10"
|
CornerRadius="10"
|
||||||
|
|||||||
@@ -2,6 +2,8 @@
|
|||||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Nebula.Launcher", "Nebula.Launcher\Nebula.Launcher.csproj", "{D8F9728D-6153-4351-8BE2-52F7D54C299D}"
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Nebula.Launcher", "Nebula.Launcher\Nebula.Launcher.csproj", "{D8F9728D-6153-4351-8BE2-52F7D54C299D}"
|
||||||
EndProject
|
EndProject
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Robust.LoaderApi", "Robust.LoaderApi\Robust.LoaderApi\Robust.LoaderApi.csproj", "{8AE91631-DE96-4A97-A255-058E27A7C3EA}"
|
||||||
|
EndProject
|
||||||
Global
|
Global
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
Debug|Any CPU = Debug|Any CPU
|
Debug|Any CPU = Debug|Any CPU
|
||||||
@@ -12,5 +14,9 @@ Global
|
|||||||
{D8F9728D-6153-4351-8BE2-52F7D54C299D}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{D8F9728D-6153-4351-8BE2-52F7D54C299D}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
{D8F9728D-6153-4351-8BE2-52F7D54C299D}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{D8F9728D-6153-4351-8BE2-52F7D54C299D}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
{D8F9728D-6153-4351-8BE2-52F7D54C299D}.Release|Any CPU.Build.0 = Release|Any CPU
|
{D8F9728D-6153-4351-8BE2-52F7D54C299D}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{8AE91631-DE96-4A97-A255-058E27A7C3EA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{8AE91631-DE96-4A97-A255-058E27A7C3EA}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{8AE91631-DE96-4A97-A255-058E27A7C3EA}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{8AE91631-DE96-4A97-A255-058E27A7C3EA}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
EndGlobal
|
EndGlobal
|
||||||
|
|||||||
1
Robust.LoaderApi
Submodule
1
Robust.LoaderApi
Submodule
Submodule Robust.LoaderApi added at 86a02eef16
Reference in New Issue
Block a user