- tweak: rework FileApi for services

- tweak: rework filter think
- add: content view button
- fix: little fixes in services
This commit is contained in:
2025-05-02 20:06:33 +03:00
parent ef8ee5a8d3
commit f066bb1188
18 changed files with 426 additions and 268 deletions

View File

@@ -0,0 +1,72 @@
using System;
using System.Windows.Input;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Layout;
namespace Nebula.Launcher.Controls;
public class FilterBox : UserControl
{
public static readonly StyledProperty<ICommand> FilterCommandProperty =
AvaloniaProperty.Register<FilterBox, ICommand>(nameof(FilterCommand));
public ICommand FilterCommand
{
get => GetValue(FilterCommandProperty);
set => SetValue(FilterCommandProperty, value);
}
public Action<FilterBoxChangedEventArgs>? OnFilterChanged {get; set;}
public string? FilterBoxName {
set => filterName.Text = value;
get => filterName.Text;
}
private StackPanel filterPanel;
private TextBox filterName = new TextBox();
public FilterBox()
{
filterPanel = new StackPanel()
{
Orientation = Orientation.Horizontal,
Spacing = 5,
};
Content = filterPanel;
}
public void AddFilter(string name, string tag)
{
var checkBox = new CheckBox();
checkBox.Content = new TextBlock()
{
Text = name,
};
checkBox.IsCheckedChanged += (_, _) =>
{
var args = new FilterBoxChangedEventArgs(tag, checkBox.IsChecked ?? false);
OnFilterChanged?.Invoke(args);
FilterCommand?.Execute(args);
};
filterPanel.Children.Add(checkBox);
}
}
public sealed class FilterBoxChangedEventArgs : EventArgs
{
public FilterBoxChangedEventArgs(string name, bool @checked)
{
Tag = name;
Checked = @checked;
}
public string Tag {get; private set;}
public bool Checked {get; private set;}
}

View File

@@ -39,6 +39,7 @@ public partial class MainViewModel : ViewModelBase
[GenerateProperty] private DebugService DebugService { get; } = default!;
[GenerateProperty] private PopupMessageService PopupMessageService { get; } = default!;
[GenerateProperty] private ContentService ContentService { get; } = default!;
[GenerateProperty, DesignConstruct] private ViewHelperService ViewHelperService { get; } = default!;
[GenerateProperty] private FileService FileService { get; } = default!;
@@ -46,9 +47,8 @@ public partial class MainViewModel : ViewModelBase
protected override void InitialiseInDesignMode()
{
CurrentPage = ViewHelperService.GetViewModel<AccountInfoViewModel>();
Items = new ObservableCollection<ListItemTemplate>(_templates);
SelectedListItem = Items.First(vm => vm.ModelType == typeof(AccountInfoViewModel));
RequirePage<AccountInfoViewModel>();
}
protected override void Initialise()
@@ -67,7 +67,7 @@ public partial class MainViewModel : ViewModelBase
loadingHandler.LoadingName = "Migration task, please wait...";
loadingHandler.IsCancellable = false;
if (!FileService.CheckMigration(loadingHandler))
if (!ContentService.CheckMigration(loadingHandler))
return;
OnPopupRequired(loadingHandler);
@@ -77,10 +77,36 @@ public partial class MainViewModel : ViewModelBase
{
if (value is null) return;
if (!ViewHelperService.TryGetViewModel(value.ModelType, out var vmb) || vmb is not IViewModelPage viewModelPage) return;
if (!ViewHelperService.TryGetViewModel(value.ModelType, out var vmb)) return;
viewModelPage.OnPageOpen(value.args);
CurrentPage = vmb;
OpenPage(vmb, value.args);
}
public T RequirePage<T>() where T : ViewModelBase, IViewModelPage
{
if (CurrentPage is T vam) return vam;
var page = ViewHelperService.GetViewModel<T>();
OpenPage(page, null);
return page;
}
private void OpenPage(ViewModelBase obj, object? args)
{
var tabItems = Items.Where(vm => vm.ModelType == obj.GetType());
var listItemTemplates = tabItems as ListItemTemplate[] ?? tabItems.ToArray();
if (listItemTemplates.Length != 0)
{
SelectedListItem = listItemTemplates.First();
}
if (obj is IViewModelPage page)
{
page.OnPageOpen(args);
}
CurrentPage = obj;
}
public void PopupMessage(PopupViewModelBase viewModelBase)

View File

@@ -146,7 +146,14 @@ public sealed partial class ContentBrowserViewModel : ViewModelBase , IViewModel
private void FillRoot(IEnumerable<ServerHubInfo> infos)
{
foreach (var info in infos) _root.Add(new ContentEntry(this, info.StatusData.Name, info.Address, info.Address, default!));
foreach (var info in infos)
_root.Add(new ContentEntry(this, info.StatusData.Name, info.Address, info.Address, default!));
}
public void Go(string server, ContentPath path)
{
ServerText = server;
Go(path);
}
public async void Go(ContentPath path)
@@ -384,6 +391,11 @@ public struct ContentPath
{
public List<string> Pathes { get; }
public ContentPath()
{
Pathes = [];
}
public ContentPath(List<string> pathes)
{
Pathes = pathes;

View File

@@ -31,6 +31,8 @@ public partial class ServerListViewModel
s.IsFavorite = true;
FavoriteServers.Add(s);
}
ApplyFilter();
}
public void AddFavorite(ServerEntryModelView entryModelView)

View File

@@ -5,6 +5,7 @@ using System.Collections.ObjectModel;
using System.Linq;
using System.Threading.Tasks;
using CommunityToolkit.Mvvm.ComponentModel;
using Nebula.Launcher.Controls;
using Nebula.Launcher.Services;
using Nebula.Launcher.ViewModels.Popup;
using Nebula.Launcher.Views;
@@ -27,6 +28,7 @@ public partial class ServerListViewModel : ViewModelBase, IViewModelPage
public ObservableCollection<ServerEntryModelView> Servers { get; }= new();
public ObservableCollection<Exception> HubErrors { get; } = new();
public readonly ServerFilter CurrentFilter = new ServerFilter();
public Action? OnSearchChange;
[GenerateProperty] private HubService HubService { get; }
@@ -38,8 +40,6 @@ public partial class ServerListViewModel : ViewModelBase, IViewModelPage
private ServerViewContainer ServerViewContainer { get; set; }
private List<ServerHubInfo> UnsortedServers { get; } = new();
private List<string> _filters = new();
//Design think
protected override void InitialiseInDesignMode()
@@ -63,23 +63,22 @@ public partial class ServerListViewModel : ViewModelBase, IViewModelPage
if (!HubService.IsUpdating) UpdateServerEntries();
UpdateFavoriteEntries();
}
public void OnFilterChanged(string name, bool active)
public void ApplyFilter()
{
DebugService.Debug($"OnFilterChanged: {name} {active}");
if(active)
_filters.Add(name);
else
_filters.Remove(name);
if(IsFavoriteMode)
foreach (var entry in ServerViewContainer.Items)
{
UpdateFavoriteEntries();
entry.ProcessFilter(CurrentFilter);
}
}
public void OnFilterChanged(FilterBoxChangedEventArgs args)
{
if (args.Checked)
CurrentFilter.Tags.Add(args.Tag);
else
{
UpdateServerEntries();
}
CurrentFilter.Tags.Remove(args.Tag);
ApplyFilter();
}
private void HubServerLoadingError(Exception obj)
@@ -96,26 +95,19 @@ public partial class ServerListViewModel : ViewModelBase, IViewModelPage
Task.Run(() =>
{
UnsortedServers.Sort(new ServerComparer());
foreach (var info in UnsortedServers.Where(CheckServerThink))
foreach (var info in UnsortedServers)
{
var view = ServerViewContainer.Get(info.Address.ToRobustUrl(), info.StatusData);
Servers.Add(view);
}
ApplyFilter();
});
}
private void OnChangeSearch()
{
if(string.IsNullOrEmpty(SearchText)) return;
if(IsFavoriteMode)
{
UpdateFavoriteEntries();
}
else
{
UpdateServerEntries();
}
CurrentFilter.SearchText = SearchText;
ApplyFilter();
}
private void HubServerChangedEventArgs(HubServerChangedEventArgs obj)
@@ -134,18 +126,6 @@ public partial class ServerListViewModel : ViewModelBase, IViewModelPage
}
}
private bool CheckServerThink(ServerHubInfo hubInfo)
{
var isNameEqual = string.IsNullOrEmpty(SearchText) || hubInfo.StatusData.Name.ToLower().Contains(SearchText.ToLower());
if (_filters.Count == 0) return isNameEqual;
if(_filters.Select(t=>t.Replace('_',':').Replace("ERPYes","18+")).Any(t=>hubInfo.StatusData.Tags.Contains(t) || hubInfo.InferredTags.Contains(t)))
return isNameEqual;
return false;
}
public void FilterRequired()
{
IsFilterVisible = !IsFilterVisible;
@@ -178,6 +158,8 @@ public class ServerViewContainer(
)
{
private readonly Dictionary<string, ServerEntryModelView> _entries = new();
public ICollection<ServerEntryModelView> Items => _entries.Values;
public void Clear()
{
@@ -240,4 +222,31 @@ public class ServerComparer : IComparer<ServerHubInfo>, IComparer<ServerStatus>,
{
return Compare(x.Item2, y.Item2);
}
}
public sealed class ServerFilter
{
public string SearchText { get; set; } = "";
public HashSet<string> Tags { get; } = new();
public bool IsMatchByName(string name)
{
if (string.IsNullOrWhiteSpace(SearchText))
return true;
return name.Contains(SearchText, StringComparison.OrdinalIgnoreCase);
}
public bool IsMatchByTags(IEnumerable<string> itemTags)
{
if (Tags.Count == 0)
return true;
var itemTagSet = new HashSet<string>(itemTags);
return Tags.All(tag => itemTagSet.Contains(tag));
}
public bool IsMatch(string name, IEnumerable<string> itemTags)
{
return IsMatchByName(name) && IsMatchByTags(itemTags);
}
}

View File

@@ -2,13 +2,16 @@ using System;
using System.Collections.ObjectModel;
using System.Diagnostics;
using System.IO;
using System.Reflection;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using System.Windows.Input;
using Avalonia.Controls;
using Avalonia.Media;
using CommunityToolkit.Mvvm.ComponentModel;
using Nebula.Launcher.Services;
using Nebula.Launcher.ViewModels.Pages;
using Nebula.Launcher.ViewModels.Popup;
using Nebula.Launcher.Views;
using Nebula.Shared.Models;
@@ -17,15 +20,25 @@ using Nebula.Shared.Utils;
namespace Nebula.Launcher.ViewModels;
[ViewModelRegister(typeof(ServerEntryView), isSingleton: false)]
[ViewModelRegister(typeof(ServerEntryView), false)]
[ConstructGenerator]
public partial class ServerEntryModelView : ViewModelBase
{
[ObservableProperty] private string _description = "Fetching info...";
[ObservableProperty] private bool _expandInfo;
[ObservableProperty] private bool _isFavorite;
[ObservableProperty] private bool _isVisible;
private string _lastError = "";
private Process? _p;
public RobustUrl Address { get; private set; }
public Action? OnFavoriteToggle;
private ServerInfo? _serverInfo;
[ObservableProperty] private bool _tagDataVisible;
public LogPopupModelView CurrLog;
public Action? OnFavoriteToggle;
public RobustUrl Address { get; private set; }
[GenerateProperty] private AuthService AuthService { get; } = default!;
[GenerateProperty] private ContentService ContentService { get; } = default!;
[GenerateProperty] private CancellationService CancellationService { get; } = default!;
@@ -34,53 +47,27 @@ public partial class ServerEntryModelView : ViewModelBase
[GenerateProperty] private PopupMessageService PopupMessageService { get; } = default!;
[GenerateProperty] private ViewHelperService ViewHelperService { get; } = default!;
[GenerateProperty] private RestService RestService { get; } = default!;
[GenerateProperty] private MainViewModel MainViewModel { get; } = default!;
[ObservableProperty] private string _description = "Fetching info...";
[ObservableProperty] private bool _expandInfo = false;
[ObservableProperty] private bool _tagDataVisible = false;
[ObservableProperty] private bool _isFavorite = false;
public ServerStatus Status { get; private set; } =
new ServerStatus(
"Fetching data...",
$"Loading...", [],
"",
-1,
-1,
-1,
public ServerStatus Status { get; private set; } =
new(
"Fetching data...",
"Loading...", [],
"",
-1,
-1,
-1,
false,
DateTime.Now,
-1
);
public ObservableCollection<ServerLink> Links { get; } = new();
public bool RunVisible => Process == null;
private ServerInfo? _serverInfo = null;
private string _lastError = "";
public async Task<ServerInfo?> GetServerInfo()
{
if (_serverInfo == null)
{
try
{
_serverInfo = await RestService.GetAsync<ServerInfo>(Address.InfoUri, CancellationService.Token);
}
catch (Exception e)
{
Description = e.Message;
DebugService.Error(e);
}
}
return _serverInfo;
}
public ObservableCollection<string> Tags { get; } = [];
public ICommand OnLinkGo { get; }= new LinkGoCommand();
public ICommand OnLinkGo { get; } = new LinkGoCommand();
private Process? Process
{
@@ -92,10 +79,26 @@ public partial class ServerEntryModelView : ViewModelBase
}
}
public async Task<ServerInfo?> GetServerInfo()
{
if (_serverInfo == null)
try
{
_serverInfo = await RestService.GetAsync<ServerInfo>(Address.InfoUri, CancellationService.Token);
}
catch (Exception e)
{
Description = e.Message;
DebugService.Error(e);
}
return _serverInfo;
}
protected override void InitialiseInDesignMode()
{
Description = "Server of meow girls! Nya~ \nNyaMeow\nOOOINK!!";
Links.Add(new ServerLink("Discord","discord","https://cinka.ru"));
Links.Add(new ServerLink("Discord", "discord", "https://cinka.ru"));
Status = new ServerStatus("Ameba",
"Locala meow meow meow meow meow meow meow meow meow meow meow meow meow meow meow meow meow meow meow meow meow meow meow meow meow meow meow meow meow meow meow meow meow meow meow meow ",
["rp:hrp", "18+"],
@@ -109,14 +112,16 @@ public partial class ServerEntryModelView : ViewModelBase
CurrLog = ViewHelperService.GetViewModel<LogPopupModelView>();
}
public void ProcessFilter(ServerFilter serverFilter)
{
IsVisible = serverFilter.IsMatch(Status.Name, Tags);
}
public void SetStatus(ServerStatus serverStatus)
{
Status = serverStatus;
Tags.Clear();
foreach (var tag in Status.Tags)
{
Tags.Add(tag);
}
foreach (var tag in Status.Tags) Tags.Add(tag);
OnPropertyChanged(nameof(Status));
}
@@ -124,13 +129,9 @@ public partial class ServerEntryModelView : ViewModelBase
{
Address = url;
if (serverStatus is not null)
{
SetStatus(serverStatus);
}
else
{
FetchStatus();
}
return this;
}
@@ -149,8 +150,13 @@ public partial class ServerEntryModelView : ViewModelBase
-1);
}
}
public void OpenContentViewer()
{
MainViewModel.RequirePage<ContentBrowserViewModel>().Go(Address.ToString(), new ContentPath());
}
public void ToggleFavorites()
{
OnFavoriteToggle?.Invoke();
@@ -179,8 +185,8 @@ public partial class ServerEntryModelView : ViewModelBase
await RunnerService.PrepareRun(buildInfo, loadingContext, CancellationService.Token);
var path = Path.GetDirectoryName(System.Reflection.Assembly.GetEntryAssembly().Location);
var path = Path.GetDirectoryName(Assembly.GetEntryAssembly().Location);
Process = Process.Start(new ProcessStartInfo
{
FileName = "dotnet.exe",
@@ -236,7 +242,7 @@ public partial class ServerEntryModelView : ViewModelBase
DebugService.Log("PROCESS EXIT WITH CODE " + Process.ExitCode);
if(Process.ExitCode != 0)
if (Process.ExitCode != 0)
PopupMessageService.Popup($"Game exit with code {Process.ExitCode}.\nReason: {_lastError}");
Process.Dispose();
@@ -261,7 +267,7 @@ public partial class ServerEntryModelView : ViewModelBase
CurrLog.Append(e.Data);
}
}
public void ReadLog()
{
PopupMessageService.Popup(CurrLog);
@@ -275,25 +281,16 @@ public partial class ServerEntryModelView : ViewModelBase
public async void ExpandInfoRequired()
{
ExpandInfo = !ExpandInfo;
if (Avalonia.Controls.Design.IsDesignMode)
{
return;
}
if (Design.IsDesignMode) return;
var info = await GetServerInfo();
if (info == null)
{
return;
}
if (info == null) return;
Description = info.Desc;
Links.Clear();
if(info.Links is null) return;
foreach (var link in info.Links)
{
Links.Add(link);
}
if (info.Links is null) return;
foreach (var link in info.Links) Links.Add(link);
}
private static string FindDotnetPath()
@@ -353,6 +350,7 @@ public class LinkGoCommand : ICommand
{
CanExecuteChanged?.Invoke(this, EventArgs.Empty);
}
public bool CanExecute(object? parameter)
{
return true;
@@ -360,7 +358,7 @@ public class LinkGoCommand : ICommand
public void Execute(object? parameter)
{
if(parameter is not string str) return;
if (parameter is not string str) return;
Helper.SafeOpenBrowser(str);
}

View File

@@ -120,7 +120,7 @@
https://cinka.ru/nebula-launcher/
</TextBlock>
</Button>
<TextBlock HorizontalAlignment="Right" VerticalAlignment="Center">v0.05-a</TextBlock>
<TextBlock HorizontalAlignment="Right" VerticalAlignment="Center">v0.08-a</TextBlock>
</Panel>
</Label>
</Border>

View File

@@ -8,7 +8,8 @@
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:pages="clr-namespace:Nebula.Launcher.ViewModels.Pages"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:controls="clr-namespace:Nebula.Launcher.Controls">
<Design.DataContext>
<pages:ServerListViewModel />
@@ -22,7 +23,7 @@
Grid.RowSpan="2"
Margin="5,0,0,10"
Padding="0,0,10,0">
<StackPanel>
<StackPanel Margin="0,0,0,30">
<ItemsControl ItemsSource="{Binding HubErrors}" Margin="10,0,10,0" />
<ItemsControl
IsVisible="{Binding IsFavoriteMode}"
@@ -41,19 +42,8 @@
VerticalAlignment="Bottom"
IsVisible="{Binding IsFilterVisible}">
<StackPanel Orientation="Vertical" Spacing="2" Margin="15">
<StackPanel Orientation="Horizontal" Spacing="15">
<TextBlock Text="Roleplay:" VerticalAlignment="Center" Margin="0,0,15,0"/>
<CheckBox Click="Button_OnClick" Name="rp_none"><TextBlock Text="NonRP"/></CheckBox>
<CheckBox Click="Button_OnClick" Name="rp_low"><TextBlock Text="LowRP"/></CheckBox>
<CheckBox Click="Button_OnClick" Name="rp_med"><TextBlock Text="MediumRP"/></CheckBox>
<CheckBox Click="Button_OnClick" Name="rp_high"><TextBlock Text="HardRP"/></CheckBox>
<CheckBox Click="Button_OnClick" Name="ERPYes"><TextBlock Text="18+"/></CheckBox>
</StackPanel>
<StackPanel Orientation="Horizontal" Spacing="15">
<TextBlock Text="Language:" VerticalAlignment="Center" Margin="0,0,4,0"/>
<CheckBox Click="Button_OnClick" Name="lang_ru"><TextBlock Text="RU"/></CheckBox>
<CheckBox Click="Button_OnClick" Name="lang_en"><TextBlock Text="EN"/></CheckBox>
</StackPanel>
<controls:FilterBox Name="EssentialFilters" FilterBoxName="Roleplay" FilterCommand="{Binding OnFilterChanged}"/>
<controls:FilterBox Name="LanguageFilters" FilterBoxName="Language" FilterCommand="{Binding OnFilterChanged}"/>
</StackPanel>
</Border>

View File

@@ -1,7 +1,4 @@
using System;
using System.Collections.Generic;
using Avalonia.Controls;
using Avalonia.Interactivity;
using ServerListViewModel = Nebula.Launcher.ViewModels.Pages.ServerListViewModel;
namespace Nebula.Launcher.Views.Pages;
@@ -12,6 +9,15 @@ public partial class ServerListView : UserControl
public ServerListView()
{
InitializeComponent();
EssentialFilters.AddFilter("Non RP", "rp:none");
EssentialFilters.AddFilter("Low RP", "rp:low");
EssentialFilters.AddFilter("Medium RP", "rp:med");
EssentialFilters.AddFilter("Hard RP", "rp:high");
EssentialFilters.AddFilter("18+", "18+");
LanguageFilters.AddFilter("RU","lang:ru");
LanguageFilters.AddFilter("EN","lang:en");
}
// This constructor is used when the view is created via dependency injection
@@ -26,11 +32,4 @@ public partial class ServerListView : UserControl
var context = (ServerListViewModel?)DataContext;
context?.OnSearchChange?.Invoke();
}
private void Button_OnClick(object? sender, RoutedEventArgs e)
{
var send = sender as CheckBox;
var context = (ServerListViewModel?)DataContext;
context?.OnFilterChanged(send.Name, send.IsChecked.Value);
}
}

View File

@@ -5,7 +5,6 @@
x:Class="Nebula.Launcher.Views.ServerEntryView"
x:DataType="viewModels:ServerEntryModelView"
xmlns="https://github.com/avaloniaui"
xmlns:asyncImageLoader="clr-namespace:AsyncImageLoader;assembly=AsyncImageLoader.Avalonia"
xmlns:converters="clr-namespace:Nebula.Launcher.Converters"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
@@ -13,7 +12,8 @@
xmlns:system="clr-namespace:System;assembly=System.Runtime"
xmlns:viewModels="clr-namespace:Nebula.Launcher.ViewModels"
xmlns:views="clr-namespace:Nebula.Launcher.Views"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
IsVisible="{Binding IsVisible}">
<Design.DataContext>
<viewModels:ServerEntryModelView />
</Design.DataContext>
@@ -234,6 +234,10 @@
IsVisible="{Binding ExpandInfo}"
Margin="5,5,0,0"
Spacing="5">
<Button
Command="{Binding OpenContentViewer}">
<Svg Margin="4" Path="/Assets/svg/folder.svg" />
</Button>
<Button
Command="{Binding StopInstance}"
CornerRadius="10"

View File

@@ -5,11 +5,11 @@ namespace Nebula.Shared;
public static class CurrentConVar
{
public static readonly ConVar<string> EngineManifestUrl =
ConVarBuilder.Build("engine.manifestUrl", "https://robust-builds.cdn.spacestation14.com/manifest.json");
public static readonly ConVar<string[]> EngineManifestUrl =
ConVarBuilder.Build<string[]>("engine.manifestUrl", ["https://robust-builds.cdn.spacestation14.com/manifest.json"]);
public static readonly ConVar<string> EngineModuleManifestUrl =
ConVarBuilder.Build("engine.moduleManifestUrl", "https://robust-builds.cdn.spacestation14.com/modules.json");
public static readonly ConVar<string[]> EngineModuleManifestUrl =
ConVarBuilder.Build<string[]>("engine.moduleManifestUrl", ["https://robust-builds.cdn.spacestation14.com/modules.json"]);
public static readonly ConVar<int> ManifestDownloadProtocolVersion =
ConVarBuilder.Build("engine.manifestDownloadProtocolVersion", 1);

View File

@@ -1,5 +1,7 @@
using System.Diagnostics.CodeAnalysis;
using System.Text.Json;
using Nebula.Shared.FileApis.Interfaces;
using Robust.LoaderApi;
namespace Nebula.Shared.Services;
@@ -31,21 +33,24 @@ public static class ConVarBuilder
public class ConfigurationService
{
private readonly DebugService _debugService;
private readonly FileService _fileService;
public IReadWriteFileApi ConfigurationApi { get; init; }
public ConfigurationService(FileService fileService, DebugService debugService)
{
_fileService = fileService ?? throw new ArgumentNullException(nameof(fileService));
_debugService = debugService ?? throw new ArgumentNullException(nameof(debugService));
ConfigurationApi = fileService.CreateFileApi("config");
}
public T? GetConfigValue<T>(ConVar<T> conVar)
{
ArgumentNullException.ThrowIfNull(conVar);
try
{
if (_fileService.ConfigurationApi.TryOpen(GetFileName(conVar), out var stream))
if (ConfigurationApi.TryOpen(GetFileName(conVar), out var stream))
using (stream)
{
var obj = JsonSerializer.Deserialize<T>(stream);
@@ -72,7 +77,7 @@ public class ConfigurationService
value = default;
try
{
if (_fileService.ConfigurationApi.TryOpen(GetFileName(conVar), out var stream))
if (ConfigurationApi.TryOpen(GetFileName(conVar), out var stream))
using (stream)
{
var obj = JsonSerializer.Deserialize<T>(stream);
@@ -116,7 +121,7 @@ public class ConfigurationService
writer.Flush();
stream.Seek(0, SeekOrigin.Begin);
_fileService.ConfigurationApi.Save(GetFileName(conVar), stream);
ConfigurationApi.Save(GetFileName(conVar), stream);
}
catch (Exception e)
{

View File

@@ -11,9 +11,12 @@ namespace Nebula.Shared.Services;
public partial class ContentService
{
public readonly IReadWriteFileApi ContentFileApi = fileService.CreateFileApi("content");
public readonly IReadWriteFileApi ManifestFileApi = fileService.CreateFileApi("manifest");
public bool CheckManifestExist(RobustManifestItem item)
{
return fileService.ContentFileApi.Has(item.Hash);
return ContentFileApi.Has(item.Hash);
}
public async Task<HashApi> EnsureItems(ManifestReader manifestReader, Uri downloadUri,
@@ -31,7 +34,7 @@ public partial class ContentService
allItems.Add(item.Value);
}
var hashApi = new HashApi(allItems, fileService.ContentFileApi);
var hashApi = new HashApi(allItems, ContentFileApi);
items = allItems.Where(a=> !hashApi.Has(a)).ToList();
@@ -46,7 +49,7 @@ public partial class ContentService
{
debugService.Log("Getting manifest: " + info.Hash);
if (fileService.ManifestFileApi.TryOpen(info.Hash, out var stream))
if (ManifestFileApi.TryOpen(info.Hash, out var stream))
{
debugService.Log("Loading manifest from: " + info.Hash);
return await EnsureItems(new ManifestReader(stream), info.DownloadUri, loadingHandler, cancellationToken);
@@ -58,7 +61,7 @@ public partial class ContentService
if (!response.IsSuccessStatusCode) throw new Exception();
await using var streamContent = await response.Content.ReadAsStreamAsync(cancellationToken);
fileService.ManifestFileApi.Save(info.Hash, streamContent);
ManifestFileApi.Save(info.Hash, streamContent);
streamContent.Seek(0, SeekOrigin.Begin);
using var manifestReader = new ManifestReader(streamContent);
return await EnsureItems(manifestReader, info.DownloadUri, loadingHandler, cancellationToken);
@@ -90,8 +93,6 @@ public partial class ContentService
loadingHandler.AppendResolvedJob();
});
if (loadingHandler is IDisposable disposable)
{

View File

@@ -0,0 +1,45 @@
using Nebula.Shared.FileApis;
using Nebula.Shared.Models;
namespace Nebula.Shared.Services;
public partial class ContentService
{
public bool CheckMigration(ILoadingHandler loadingHandler)
{
debugService.Log("Checking migration...");
var migrationList = ContentFileApi.AllFiles.Where(f => !f.Contains("\\")).ToList();
if(migrationList.Count == 0) return false;
debugService.Log($"Found {migrationList.Count} migration files. Starting migration...");
Task.Run(() => DoMigration(loadingHandler, migrationList));
return true;
}
private void DoMigration(ILoadingHandler loadingHandler, List<string> migrationList)
{
loadingHandler.SetJobsCount(migrationList.Count);
Parallel.ForEach(migrationList, (f,_)=>MigrateFile(f,loadingHandler));
if (loadingHandler is IDisposable disposable)
{
disposable.Dispose();
}
}
private void MigrateFile(string file, ILoadingHandler loadingHandler)
{
if(!ContentFileApi.TryOpen(file, out var stream))
{
loadingHandler.AppendResolvedJob();
return;
}
ContentFileApi.Save(HashApi.GetManifestPath(file), stream);
stream.Dispose();
ContentFileApi.Remove(file);
loadingHandler.AppendResolvedJob();
}
}

View File

@@ -1,5 +1,6 @@
using System.Diagnostics.CodeAnalysis;
using Nebula.Shared.FileApis;
using Nebula.Shared.FileApis.Interfaces;
using Nebula.Shared.Models;
using Nebula.Shared.Utils;
@@ -12,80 +13,68 @@ public sealed class EngineService
private readonly DebugService _debugService;
private readonly FileService _fileService;
private readonly RestService _restService;
private readonly IServiceProvider _serviceProvider;
private readonly ConfigurationService _varService;
private readonly Task _currInfoTask;
public Dictionary<string, Module> ModuleInfos = default!;
public Dictionary<string, EngineVersionInfo> VersionInfos = default!;
private readonly IReadWriteFileApi _engineFileApi;
private ModulesInfo _modulesInfo = default!;
private Dictionary<string, EngineVersionInfo> _versionsInfo = default!;
public EngineService(RestService restService, DebugService debugService, ConfigurationService varService,
FileService fileService, IServiceProvider serviceProvider, AssemblyService assemblyService)
FileService fileService, AssemblyService assemblyService)
{
_restService = restService;
_debugService = debugService;
_varService = varService;
_fileService = fileService;
_serviceProvider = serviceProvider;
_assemblyService = assemblyService;
_engineFileApi = fileService.CreateFileApi("engine");
_currInfoTask = Task.Run(() => LoadEngineManifest(CancellationToken.None));
}
public async Task LoadEngineManifest(CancellationToken cancellationToken)
{
try
_versionsInfo = await LoadExacManifest(CurrentConVar.EngineManifestUrl, CurrentConVar.EngineManifestBackup, cancellationToken);
_modulesInfo = await LoadExacManifest(CurrentConVar.EngineModuleManifestUrl, CurrentConVar.ModuleManifestBackup, cancellationToken);
}
private async Task<T> LoadExacManifest<T>(ConVar<string[]> conVar,ConVar<T> backup,CancellationToken cancellationToken)
{
var manifestUrls = _varService.GetConfigValue(conVar)!;
foreach (var manifestUrl in manifestUrls)
{
_debugService.Log("Fetching engine manifest from: " + CurrentConVar.EngineManifestUrl);
var info = await _restService.GetAsync<Dictionary<string, EngineVersionInfo>>(
new Uri(_varService.GetConfigValue(CurrentConVar.EngineManifestUrl)!), cancellationToken);
VersionInfos = info;
_varService.SetConfigValue(CurrentConVar.EngineManifestBackup, info);
}
catch (Exception e)
{
_debugService.Debug("Trying fallback engine manifest...");
if (!_varService.TryGetConfigValue(CurrentConVar.EngineManifestBackup, out var engineInfo))
try
{
throw new Exception("No engine info data available",e);
}
VersionInfos = engineInfo;
}
try
{
_debugService.Log("Fetching module manifest from: " + CurrentConVar.EngineModuleManifestUrl);
var moduleInfo = await _restService.GetAsync<ModulesInfo>(
new Uri(_varService.GetConfigValue(CurrentConVar.EngineModuleManifestUrl)!), cancellationToken);
if (moduleInfo is null)
throw new Exception("Module version info is null");
_debugService.Log("Fetching engine manifest from: " + manifestUrl);
var info = await _restService.GetAsync<T>(
new Uri(manifestUrl), cancellationToken);
ModuleInfos = moduleInfo.Modules;
_varService.SetConfigValue(CurrentConVar.ModuleManifestBackup, moduleInfo);
}
catch (Exception e)
{
_debugService.Debug("Trying fallback module manifest...");
if (!_varService.TryGetConfigValue(CurrentConVar.ModuleManifestBackup, out var modulesInfo))
{
throw new Exception("No module info data available",e);
_varService.SetConfigValue(backup, info);
return info;
}
catch (Exception e)
{
_debugService.Error($"error while attempt fetch engine manifest: {e.Message}");
}
ModuleInfos = modulesInfo.Modules;
}
_debugService.Debug("Trying fallback module manifest...");
if (!_varService.TryGetConfigValue(backup, out var moduleInfo))
{
throw new Exception("No module info data available");
}
return moduleInfo;
}
public EngineBuildInfo? GetVersionInfo(string version)
{
CheckAndWaitValidation();
if (!VersionInfos.TryGetValue(version, out var foundVersion))
if (!_versionsInfo.TryGetValue(version, out var foundVersion))
return null;
if (foundVersion.RedirectVersion != null)
@@ -113,12 +102,12 @@ public sealed class EngineService
try
{
var api = _fileService.OpenZip(version, _fileService.EngineFileApi);
var api = _fileService.OpenZip(version, _engineFileApi);
if (api != null) return _assemblyService.Mount(api);
}
catch (Exception)
{
_fileService.EngineFileApi.Remove(version);
_engineFileApi.Remove(version);
throw;
}
@@ -133,13 +122,13 @@ public sealed class EngineService
_debugService.Log("Downloading engine version " + version);
using var client = new HttpClient();
var s = await client.GetStreamAsync(info.Url);
_fileService.EngineFileApi.Save(version, s);
_engineFileApi.Save(version, s);
await s.DisposeAsync();
}
public bool TryOpen(string version, [NotNullWhen(true)] out Stream? stream)
{
return _fileService.EngineFileApi.TryOpen(version, out stream);
return _engineFileApi.TryOpen(version, out stream);
}
public bool TryOpen(string version)
@@ -153,7 +142,7 @@ public sealed class EngineService
{
CheckAndWaitValidation();
if (!ModuleInfos.TryGetValue(moduleName, out var module) ||
if (!_modulesInfo.Modules.TryGetValue(moduleName, out var module) ||
!module.Versions.TryGetValue(version, out var value))
return null;
@@ -174,7 +163,7 @@ public sealed class EngineService
CheckAndWaitValidation();
var engineVersionObj = Version.Parse(engineVersion);
var module = ModuleInfos[moduleName];
var module = _modulesInfo.Modules[moduleName];
var selectedVersion = module.Versions.Select(kv => new { Version = Version.Parse(kv.Key), kv.Key, kv })
.Where(kv => engineVersionObj >= kv.Version)
.MaxBy(kv => kv.Version);
@@ -196,11 +185,11 @@ public sealed class EngineService
try
{
return _assemblyService.Mount(_fileService.OpenZip(fileName, _fileService.EngineFileApi) ?? throw new InvalidOperationException($"{fileName} is not exist!"));
return _assemblyService.Mount(_fileService.OpenZip(fileName, _engineFileApi) ?? throw new InvalidOperationException($"{fileName} is not exist!"));
}
catch (Exception)
{
_fileService.EngineFileApi.Remove(fileName);
_engineFileApi.Remove(fileName);
throw;
}
}
@@ -213,7 +202,7 @@ public sealed class EngineService
_debugService.Log("Downloading engine module version " + moduleVersion);
using var client = new HttpClient();
var s = await client.GetStreamAsync(info.Url);
_fileService.EngineFileApi.Save(ConcatName(moduleName, moduleVersion), s);
_engineFileApi.Save(ConcatName(moduleName, moduleVersion), s);
await s.DisposeAsync();
}

View File

@@ -14,62 +14,15 @@ public class FileService
Environment.SpecialFolder.ApplicationData), "Datum");
private readonly DebugService _debugService;
public readonly IReadWriteFileApi ConfigurationApi;
public readonly IReadWriteFileApi ContentFileApi;
public readonly IReadWriteFileApi EngineFileApi;
public readonly IReadWriteFileApi ManifestFileApi;
public FileService(DebugService debugService)
{
_debugService = debugService;
if(!Directory.Exists(RootPath))
Directory.CreateDirectory(RootPath);
ContentFileApi = CreateFileApi("content");
EngineFileApi = CreateFileApi("engine");
ManifestFileApi = CreateFileApi("manifest");
ConfigurationApi = CreateFileApi("config");
}
public bool CheckMigration(ILoadingHandler loadingHandler)
{
_debugService.Log("Checking migration...");
var migrationList = ContentFileApi.AllFiles.Where(f => !f.Contains("\\")).ToList();
if(migrationList.Count == 0) return false;
_debugService.Log($"Found {migrationList.Count} migration files. Starting migration...");
Task.Run(() => DoMigration(loadingHandler, migrationList));
return true;
}
private void DoMigration(ILoadingHandler loadingHandler, List<string> migrationList)
{
loadingHandler.SetJobsCount(migrationList.Count);
Parallel.ForEach(migrationList, (f,_)=>MigrateFile(f,loadingHandler));
if (loadingHandler is IDisposable disposable)
{
disposable.Dispose();
}
}
private void MigrateFile(string file, ILoadingHandler loadingHandler)
{
if(!ContentFileApi.TryOpen(file, out var stream))
{
loadingHandler.AppendResolvedJob();
return;
}
ContentFileApi.Save(HashApi.GetManifestPath(file), stream);
stream.Dispose();
ContentFileApi.Remove(file);
loadingHandler.AppendResolvedJob();
}
public IReadWriteFileApi CreateFileApi(string path)
{
return new FileApi(Path.Join(RootPath, path));

View File

@@ -43,11 +43,21 @@ public sealed class RunnerService(
new(hashApi, "/")
};
var module =
await engineService.EnsureEngineModules("Robust.Client.WebView", buildInfo.BuildInfo.Build.EngineVersion);
if (module is not null)
extraMounts.Add(new ApiMount(module, "/"));
if (hashApi.TryOpen("manifest.yml", out var stream))
{
var modules = ContentManifestParser.ExtractModules(stream);
foreach (var moduleStr in modules)
{
var module =
await engineService.EnsureEngineModules(moduleStr, buildInfo.BuildInfo.Build.EngineVersion);
if (module is not null)
extraMounts.Add(new ApiMount(module, "/"));
}
await stream.DisposeAsync();
}
var args = new MainArgs(runArgs, engine, redialApi, extraMounts);
if (!assemblyService.TryOpenAssembly(varService.GetConfigValue(CurrentConVar.RobustAssemblyName)!, engine,
@@ -59,4 +69,46 @@ public sealed class RunnerService(
await Task.Run(() => loader.Main(args), cancellationToken);
}
}
public static class ContentManifestParser
{
public static List<string> ExtractModules(Stream manifestStream)
{
using var reader = new StreamReader(manifestStream);
return ExtractModules(reader.ReadToEnd());
}
public static List<string> ExtractModules(string manifestContent)
{
var modules = new List<string>();
var lines = manifestContent.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries);
bool inModulesSection = false;
foreach (var rawLine in lines)
{
var line = rawLine.Trim();
if (line.StartsWith("modules:"))
{
inModulesSection = true;
continue;
}
if (inModulesSection)
{
if (line.StartsWith("- "))
{
modules.Add(line.Substring(2).Trim());
}
else if (!line.StartsWith(" "))
{
break;
}
}
}
return modules;
}
}

View File

@@ -1,4 +1,5 @@
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AButton_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FUsers_003FCinka_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003Fcc84c38d8785b88e166e6741b6a4c0dfa09eaf6e41eb151b255817e11f27570_003FButton_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ACancellationToken_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FUsers_003FCinka_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003F2565b9d99fdde488bc7801b84387b2cc864959cfb63212e1ff576fc9c6bb7e_003FCancellationToken_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AConsole_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FUsers_003FCinka_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003Ffd57398b7dc3a8ce7da2786f2c67289c3d974658a9e90d0c1e84db3d965fbf1_003FConsole_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AFrozenDictionary_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FUsers_003FCinka_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003F89dff9063ddb01ff8125b579122b88bf4de94526490d77bcbbef7d0ee662a_003FFrozenDictionary_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>