diff --git a/README.md b/README.md index c239d3c6..05f2bc18 100644 --- a/README.md +++ b/README.md @@ -1,42 +1,108 @@ -[![Discord Banner](https://img.shields.io/badge/discord-join%20chat-46BC99)](https://discord.gg/HVDzx4rCgg) +

+ Minecraft Holy Client +

-*На данный момент программа находится в предварительной версии, поэтому некоторые функции нестабильны. Подробнее можно узнать [здесь](preview.md).* -# Minecraft Holy Client +

A high-performance platform for running stress-testing minecraft bots.

-Высокопроизводительная платформа для запуска стресс-тест ботов Minecraft, написанная на C#. +

+ + GitHub last commit + + GitHub issues + + GitHub pull requests + + Discord + + Download -## Get Started + +

-[Скачайте в релизах](https://github.com/Titlehhhh/Minecraft-Holy-Client/releases) последнюю версию Minecraft Holy Client и запустите её. -После запуска приложения Вас встретит окно с главной страницей. -![image](https://github.com/Titlehhhh/Minecraft-Holy-Client/assets/93156853/4d582c49-13bd-44c7-81b4-f6ebd5b924cd) +

+ Installation • + Launch • + Features • + Contributing • + Custom plugin • + License +

-После открытия навигационного меню Вы увидите несколько пунктов: -- Главная. Страница где есть основная информация о приложении: GitHub, Наш Discord сервер, а также документация(в разрботке). -- Бот менеджер. Эта экспериментальная функция. Планируется в программу добавить простых ботов, таких как, боты афк-рыбалки или подобные. -- Настройки. Вы можете настроить язык. Пока можно настроить только это. -- Стресс-тест. Основная возможность приложения. Здесь Вы можете протестировать Ваш сервер под высокой нагрузкой. -- Управление расширениями. Здесь Вы можете добавить свои плагины в программу. -![image](https://github.com/Titlehhhh/Minecraft-Holy-Client/assets/93156853/3a156e06-bd3c-4882-9c5c-ce123c14b9c2) +--- -### Запуск стресс-теста -1) Перейдем на страницу Стресс-тест и введем Ip адрес сервера, который мы хотим нагрузить. -2) Далее поставить префикс ника для ботов. Пусть будет "Title_" -3) Затем укажем количество ботов. Это количество, которое система будет стремиться запустить. Например 1000. -4) Укажем, что нужно в тестировании использовать прокси сервера, а также укажем версию 1.16.5. -5) В настройках поведения укажем, чтобы боты спамили "Hello from Minecraft Holy Client". -![image](https://github.com/Titlehhhh/Minecraft-Holy-Client/assets/93156853/fdbab77e-0eed-44ed-bbe3-30e6d100802b) + + + + +
-Нажимаем на зеленую кнопку видим, что боты зашли на сервер и спамят "Hello from Minecraft Holy Client" -![image](https://github.com/Titlehhhh/Minecraft-Holy-Client/assets/93156853/51c89585-1fd5-4351-9677-a59322ececd7) +**Minecraft Holy Client** is a handy, **high-performance**, easily extensible **open-source** application designed to run load and stress-test **Minecraft** bots, written in C#. +All components of the application have been designed to **maximize performance**, so it works stably on **weak devices**. -## Roadmap -![image](https://github.com/Titlehhhh/Minecraft-Holy-Client/assets/93156853/cee54a39-b6e5-4e10-b329-ec0230eb43b4) +![Minecraft Holy Client Bots](https://github.com/Titlehhhh/Minecraft-Holy-Client/assets/93156853/75c9f0a3-8ae2-4b7c-8ad5-e8b5fa120165) +

+(Preview 300~ bots) +

+ +
+ +## Installation + +Minecraft Holy Client is available for **Windows**, **Linux** and **MacOs**. In addition, it is **portable** and is not installed on the system, so it can be easily uninstalled. + +[Download](https://github.com/Titlehhhh/Minecraft-Holy-Client/releases) the latest version of HolyClient.Desktop and follow these instructions depending on your platform. + +### Windows + +1) Open and _extract_ the archive with the program. +2) Run **HolyClient.Desktop.exe** + +### Linux + +_Using Ubuntu as an example_ + +1) Open and _extract_ the archive with the program. +2) Open a terminal where the HolyClient.Desktop file is located and execute `./HolyClient.Desktop` in the console + +### Mac Os + +1) Open and _extract_ the archive with the program. +2) Open a terminal where the HolyClient.Desktop file is located and execute `./HolyClient.Desktop` in the console + + +## Launch + +When we have launched the app, then we can safely go to the "Stress Test" tab and we can launch bots. + + + +![image](https://github.com/Titlehhhh/Minecraft-Holy-Client/assets/93156853/55769ef1-f81d-477d-9027-02dfa0339f80) + + +## Features + +- **Custom proxies**. It is possible to load custom proxies from a link and a file. +- **Multiverse in bots**. Bots can enter servers from 1.12.2 to 1.19.3. Temporarily not all packets are supported, this is being actively worked on. +- **Using user behavior for stress testing**. + +## Contributing + +Contributions are always welcome! + +## Custom plugin + +The default plugin has few features. It just sends `/register ` first and starts spamming immediately. +Minecraft servers are a large number, with different anti-bot systems and if you want to test your server for bot attacks, it makes sense to write a custom plugin that would bypass it. How to write custom plugins is written here. diff --git a/ReleaseNotes/2.0.0-preview.1/En.md b/ReleaseNotes/2.0.0-preview.1/En.md index 9975ba39..8eba07b2 100644 --- a/ReleaseNotes/2.0.0-preview.1/En.md +++ b/ReleaseNotes/2.0.0-preview.1/En.md @@ -1,7 +1,7 @@ > [!WARNING] > Disclaimer: This translation was generated using AI. -# What's New in Minecraft Holy Client *2.0.0-preview.1*? +# What's New in Minecraft Holy Client **2.0.0-preview.1.1**? ## Major Fixes diff --git a/ReleaseNotes/2.0.0-preview.1/Ru.md b/ReleaseNotes/2.0.0-preview.1/Ru.md index 34017dd4..b251d578 100644 --- a/ReleaseNotes/2.0.0-preview.1/Ru.md +++ b/ReleaseNotes/2.0.0-preview.1/Ru.md @@ -1,4 +1,4 @@ -# Что изменилось в Minecraft Holy Client *2.0.0-preview.1*? +# Что изменилось в Minecraft Holy Client **2.0.0-preview.1.1**? ## Основные исправления diff --git a/ReleaseNotes/2.0.0-preview.2/En.md b/ReleaseNotes/2.0.0-preview.2/En.md new file mode 100644 index 00000000..e69de29b diff --git a/ReleaseNotes/2.0.0-preview.2/Ru.md b/ReleaseNotes/2.0.0-preview.2/Ru.md new file mode 100644 index 00000000..d885aba8 --- /dev/null +++ b/ReleaseNotes/2.0.0-preview.2/Ru.md @@ -0,0 +1,2 @@ +- memory performance +- default plugin /reg to /register \ No newline at end of file diff --git a/build/_build.csproj b/build/_build.csproj index ece0af80..02de8215 100644 --- a/build/_build.csproj +++ b/build/_build.csproj @@ -21,7 +21,7 @@ - + diff --git a/preview.md b/preview.md index 2440d5a1..fd77f593 100644 --- a/preview.md +++ b/preview.md @@ -2,22 +2,18 @@ Этот раздел может меняться в течение разработки. Все изменения будут написаны в заметках о выпуске предварительной версии. -# Функции, которые нужно добавить -## Плагины -- Плагин, который будет установлен в программу по умолчанию. Это упростит использование программой простым пользователям. +# Плагины - Возможность перезагружать плагины из менеджера расширений, а также из вкладки "Поведения". - Возможность загружать плагины из NuGet. - Отображение ошибки, если плагин не найден. - Отображение ошибки, если поведение не найдено. -## Прокси -- Прокси по умолчанию. -- Возможность загружать прокси из URL источников, например, из открытых репозиториех GitHub с бесплатными списками прокси. +# Прокси - Возможность загружать прокси с логином и паролем. - Отображение количества загруженных прокси в окне с запуском стресс теста. - Возможность загружать разные типы прокси(HTTP(S), Socks(4/5)) из одного источника. -## Прочее +# Прочее - Система профилей в стресс тесте. diff --git a/roadmap.png b/roadmap.png new file mode 100644 index 00000000..3a07a87b Binary files /dev/null and b/roadmap.png differ diff --git a/src/CoreLibs/HolyClient.Abstractions/StressTest/IStressTestBot.cs b/src/CoreLibs/HolyClient.Abstractions/StressTest/IStressTestBot.cs index 7d5da709..544964d5 100644 --- a/src/CoreLibs/HolyClient.Abstractions/StressTest/IStressTestBot.cs +++ b/src/CoreLibs/HolyClient.Abstractions/StressTest/IStressTestBot.cs @@ -6,7 +6,7 @@ namespace HolyClient.Abstractions.StressTest public interface IStressTestBot { public Task Restart(bool changeNickAndProxy); - IObservable OnError { get; } + public MinecraftClient Client { get; } } diff --git a/src/CoreLibs/HolyClient.Common/HolyClient.Common.csproj b/src/CoreLibs/HolyClient.Common/HolyClient.Common.csproj index 4d094185..e7c29dbb 100644 --- a/src/CoreLibs/HolyClient.Common/HolyClient.Common.csproj +++ b/src/CoreLibs/HolyClient.Common/HolyClient.Common.csproj @@ -11,9 +11,9 @@ - + - + diff --git a/src/CoreLibs/HolyClient.Core/Helpers/MapDataHelper.cs b/src/CoreLibs/HolyClient.Core/Helpers/MapDataHelper.cs index 94e62762..fe692d3d 100644 --- a/src/CoreLibs/HolyClient.Core/Helpers/MapDataHelper.cs +++ b/src/CoreLibs/HolyClient.Core/Helpers/MapDataHelper.cs @@ -27,7 +27,7 @@ public static Image CreateImage(byte[] colors) var color = new Rgba32(values[0], values[1], values[2]); - //Console.WriteLine(color.Name); + image[y, x] = color; } } diff --git a/src/CoreLibs/HolyClient.Core/HolyClient.Core.csproj b/src/CoreLibs/HolyClient.Core/HolyClient.Core.csproj index 9893d896..c4aae8fd 100644 --- a/src/CoreLibs/HolyClient.Core/HolyClient.Core.csproj +++ b/src/CoreLibs/HolyClient.Core/HolyClient.Core.csproj @@ -20,9 +20,9 @@ - + - + diff --git a/src/CoreLibs/HolyClient.Core/Models/BotManager/MinecraftBot.cs b/src/CoreLibs/HolyClient.Core/Models/BotManager/MinecraftBot.cs index 847056c9..025783de 100644 --- a/src/CoreLibs/HolyClient.Core/Models/BotManager/MinecraftBot.cs +++ b/src/CoreLibs/HolyClient.Core/Models/BotManager/MinecraftBot.cs @@ -110,7 +110,7 @@ public async Task Run(ILogger logger, CancellationToken cancellation) Interlocked.Exchange(ref _cleanUpPlugins, disposables); - await minecraftClient.Login(logger); + await minecraftClient.Start(logger); } public Task Stop() { diff --git a/src/CoreLibs/HolyClient.StressTest/DefaultBehavior.cs b/src/CoreLibs/HolyClient.StressTest/DefaultBehavior.cs index 4348a0f4..a2dcc1d6 100644 --- a/src/CoreLibs/HolyClient.StressTest/DefaultBehavior.cs +++ b/src/CoreLibs/HolyClient.StressTest/DefaultBehavior.cs @@ -13,7 +13,14 @@ public class DefaultBehavior : IStressTestBehavior [System.ComponentModel.DisplayName("Spam timeout")] public int SpamTimeout { get; set; } = 5000; + [System.ComponentModel.DisplayName("Reconnect timeout")] + public int ReconnectTimeout { get; set; } = 5000; + [System.ComponentModel.DisplayName("Reconnect timeout")] + public int Reconnects { get; set; } = 1; + + [System.ComponentModel.DisplayName("Spam Nocom")] + public bool SpamNocom { get; set; } = false; private static Regex SayVerifyRegex = new(@"\.say \/verify (\d+)"); @@ -23,9 +30,9 @@ public Task Activate(CompositeDisposable disposables, IEnumerable - { + Action onErr = async (exc) => + { try { if (cts is not null) @@ -43,13 +50,35 @@ public Task Activate(CompositeDisposable disposables, IEnumerable + { + bot.Client.OnErrored -= onErr; + })); var d2 = bot.Client.OnJoinGame.Subscribe(async x => { @@ -60,12 +89,12 @@ public Task Activate(CompositeDisposable disposables, IEnumerable x.Message.Contains("verify")) @@ -76,29 +105,25 @@ public Task Activate(CompositeDisposable disposables, IEnumerable - + \ No newline at end of file diff --git a/src/CoreLibs/HolyClient.StressTest/HolyClient.StressTest.csproj b/src/CoreLibs/HolyClient.StressTest/HolyClient.StressTest.csproj index b540ee19..3663c14b 100644 --- a/src/CoreLibs/HolyClient.StressTest/HolyClient.StressTest.csproj +++ b/src/CoreLibs/HolyClient.StressTest/HolyClient.StressTest.csproj @@ -9,9 +9,9 @@ - + - + diff --git a/src/CoreLibs/HolyClient.StressTest/IProxyProvider.cs b/src/CoreLibs/HolyClient.StressTest/IProxyProvider.cs index 98964bf5..a6ddb828 100644 --- a/src/CoreLibs/HolyClient.StressTest/IProxyProvider.cs +++ b/src/CoreLibs/HolyClient.StressTest/IProxyProvider.cs @@ -2,7 +2,7 @@ namespace HolyClient.StressTest { - public interface IProxyProvider + public interface IProxyProvider : IDisposable { public ValueTask GetNextProxy(); } diff --git a/src/CoreLibs/HolyClient.StressTest/IStressTest.cs b/src/CoreLibs/HolyClient.StressTest/IStressTestProfile.cs similarity index 59% rename from src/CoreLibs/HolyClient.StressTest/IStressTest.cs rename to src/CoreLibs/HolyClient.StressTest/IStressTestProfile.cs index 7c311d12..0fac2316 100644 --- a/src/CoreLibs/HolyClient.StressTest/IStressTest.cs +++ b/src/CoreLibs/HolyClient.StressTest/IStressTestProfile.cs @@ -2,15 +2,28 @@ using HolyClient.Abstractions.StressTest; using HolyClient.Core.Infrastructure; using McProtoNet; +using System.Collections.Concurrent; using System.ComponentModel; namespace HolyClient.StressTest { + public class ExceptionThrowCount + { + public Type TypeException { get; set; } + + public int Count { get; set; } + + public Dictionary Messages { get; set; } + } + - [MessagePack.Union(0, typeof(StressTest))] - public interface IStressTest : INotifyPropertyChanged, INotifyPropertyChanging + [MessagePack.Union(0, typeof(StressTestProfile))] + public interface IStressTestProfile : INotifyPropertyChanged, INotifyPropertyChanging { + Guid Id { get; set; } + string Name { get; set; } + string Server { get; set; } @@ -23,17 +36,21 @@ public interface IStressTest : INotifyPropertyChanged, INotifyPropertyChanging bool UseProxy { get; set; } MinecraftVersion Version { get; set; } - + ISourceCache Proxies { get; } IObservable Metrics { get; } + + ConcurrentDictionary, ExceptionCounter> ExceptionCounter { get; } + IStressTestBehavior Behavior { get; } StressTestServiceState CurrentState { get; } - PluginTypeReference BehaviorRef { get; } - + PluginTypeReference BehaviorRef { get; } + bool CheckDNS { get; set; } + void SetBehavior(IPluginSource pluginSource); void DeleteBehavior(); diff --git a/src/CoreLibs/HolyClient.StressTest/ProxyProvider.cs b/src/CoreLibs/HolyClient.StressTest/ProxyProvider.cs index dfd72aa8..0fe924f5 100644 --- a/src/CoreLibs/HolyClient.StressTest/ProxyProvider.cs +++ b/src/CoreLibs/HolyClient.StressTest/ProxyProvider.cs @@ -53,7 +53,19 @@ async ValueTask IProxyProvider.GetNextProxy() _lockAsync.Release(); } - } + } + private bool disposed = false; + public void Dispose() + { + if (disposed) + return; + disposed = true; + foreach (var proxy in _clients) + { + proxy.Dispose(); + } + GC.SuppressFinalize(this); + } } } diff --git a/src/CoreLibs/HolyClient.StressTest/StressTestBot.cs b/src/CoreLibs/HolyClient.StressTest/StressTestBot.cs index d2bac8a0..954cf1a0 100644 --- a/src/CoreLibs/HolyClient.StressTest/StressTestBot.cs +++ b/src/CoreLibs/HolyClient.StressTest/StressTestBot.cs @@ -28,18 +28,17 @@ public StressTestBot(MinecraftClient client, INickProvider nickProvider, IProxyP this.cancellationToken = cancellationToken; } - private Subject _onError = new(); - - public IObservable OnError => _onError; public async Task Restart(bool changeNickAndProxy) { if (cancellationToken.IsCancellationRequested) return; + Client.Disconnect(); + + try { - Client.Disconnect(); if (changeNickAndProxy) { IProxyClient? proxy = null; @@ -58,31 +57,18 @@ public async Task Restart(bool changeNickAndProxy) }; } - await Client.Login(Logger.None); - - + await Client.Start(Logger.None); } - catch (Exception ex) + catch { + throw; + } - _onError.OnNext(ex); - - } } - private async void WaitClient() - { - try - { - await Client; - } - catch (Exception ex) - { - this._onError.OnNext(ex); - } - } + } } diff --git a/src/CoreLibs/HolyClient.StressTest/StressTest.cs b/src/CoreLibs/HolyClient.StressTest/StressTestProfile.cs similarity index 58% rename from src/CoreLibs/HolyClient.StressTest/StressTest.cs rename to src/CoreLibs/HolyClient.StressTest/StressTestProfile.cs index a9c6fbfa..f8a8bba8 100644 --- a/src/CoreLibs/HolyClient.StressTest/StressTest.cs +++ b/src/CoreLibs/HolyClient.StressTest/StressTestProfile.cs @@ -9,18 +9,44 @@ using MessagePack; using ReactiveUI; using ReactiveUI.Fody.Helpers; +using Stateless.Graph; +using System.Collections.Concurrent; using System.Diagnostics; +using System.Diagnostics.Metrics; +using System.Net.Sockets; +using System.Net; using System.Reactive.Disposables; +using System.Reactive.Linq; using System.Reactive.Subjects; +using System.Runtime.CompilerServices; +using System.Threading; namespace HolyClient.StressTest { + public class ExceptionCounter + { + private volatile int _x = 1; + + public int Count => Volatile.Read(ref this._x); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Increment() + { + Interlocked.Increment(ref _x); + } + + } + [MessagePackObject(keyAsPropertyName: true)] - public class StressTest : ReactiveObject, IStressTest + public class StressTestProfile : ReactiveObject, IStressTestProfile { #region Properties #region Serializable + public Guid Id { get; set; } = Guid.NewGuid(); + + [Reactive] + public string Name { get; set; } [Reactive] @@ -42,6 +68,9 @@ public IEnumerable ProxiesState public PluginTypeReference BehaviorRef { get; set; } [Reactive] public bool UseProxy { get; set; } = true; + + [Reactive] + public bool CheckDNS { get; set; } = false; #endregion #region NonSerializable @@ -72,7 +101,9 @@ public IStressTestBehavior Behavior [IgnoreMember] public StressTestServiceState CurrentState { get; private set; } - + + [IgnoreMember] + public ConcurrentDictionary, ExceptionCounter> ExceptionCounter { get; private set; } = new(); #endregion @@ -85,13 +116,17 @@ public IStressTestBehavior Behavior private readonly object _currentInfoLock = new(); private StressTestMetrik currentInfo; - private volatile int _botsOnlineCounter = 0; + private volatile int _botsConnectionCounter = 0; + private volatile int _botsHandshakeCounter = 0; + private volatile int _botsLoginCounter = 0; + private volatile int _botsPlayCounter = 0; + private volatile int _cpsCounter = 0; private IDisposable? _cleanUp; - public StressTest() + public StressTestProfile() { } @@ -102,9 +137,12 @@ public StressTest() [ConfigureAwait(false)] public async Task Start(Serilog.ILogger logger) { - + ExceptionCounter.Clear(); CurrentState = StressTestServiceState.Init; - _botsOnlineCounter = 0; + _botsConnectionCounter = 0; + _botsHandshakeCounter = 0; + _botsLoginCounter = 0; + _botsPlayCounter = 0; _cpsCounter = 0; try { @@ -124,6 +162,11 @@ public async Task Start(Serilog.ILogger logger) var proxyProvider = await LoadProxy(logger); + if (proxyProvider is not null) + { + _disposables.Add(proxyProvider); + } + var bots = new List(); string host = this.Server; @@ -153,9 +196,26 @@ public async Task Start(Serilog.ILogger logger) logger.Error($"[STRESS TEST] Ошибка поиска srv для {this.Server}"); } } + logger.Information($"[STRESS TEST] Поиск DNS для {this.Server}"); + if (CheckDNS) + { + try + { + var result = await Dns.GetHostAddressesAsync(host, AddressFamily.InterNetwork, cancellationTokenSource.Token).ConfigureAwait(false); + + host = result[0].ToString(); + logger.Information($"[STRESS TEST] DNS IP for {this.Server} - {host}"); + } + catch + { + logger.Error($"[STRESS TEST] Ошибка поиска DNS для {this.Server}"); + } + } logger.Information($"[STRESS TEST] Запущен стресс тест на {this.NumberOfBots} ботов на сервер {host}:{port}"); + + var stressTestBots = new List(); var nickProvider = new NickProvider(this.BotsNickname); @@ -182,60 +242,125 @@ public async Task Start(Serilog.ILogger logger) }; - bot.StateChanged += Bot_StateChanged; - _disposables.Add(Disposable.Create(() => - { - bot.StateChanged -= Bot_StateChanged; - bot.Dispose(); - })); - stressTestBots.Add( - new StressTestBot( + var b = new StressTestBot( bot, nickProvider, proxyProvider, logger, i, - cancellationTokenSource.Token)); - - } + cancellationTokenSource.Token); - new Thread(() => - { - try + Action onState = (state) => { - Stopwatch stopwatch = new(); - while (!cancellationTokenSource.IsCancellationRequested) + if (state.NewValue == ClientState.Play) { - stopwatch.Start(); - var cps = Interlocked.Exchange(ref _cpsCounter, 0); + Interlocked.Increment(ref _cpsCounter); - var botsOnline = Volatile.Read(ref _botsOnlineCounter); - _dataPerSecond.OnNext(new StressTestMetrik(cps, botsOnline)); + Interlocked.Increment(ref _botsPlayCounter); + } + else if (state.NewValue == ClientState.Connecting) + { + Interlocked.Increment(ref _botsConnectionCounter); + } + else if (state.NewValue == ClientState.HandShake) + { + Interlocked.Increment(ref _botsHandshakeCounter); + } + else if (state.NewValue == ClientState.Login) + { + Interlocked.Increment(ref _botsLoginCounter); + } + }; + Action onError = (exc) => + { - stopwatch.Stop(); + var state = b.Client.CurrentState; - if (stopwatch.Elapsed.Microseconds < 1000) - { - Thread.Sleep(1000 - stopwatch.Elapsed.Microseconds); - } - stopwatch.Reset(); + if (state == ClientState.Play) + { + Interlocked.Decrement(ref _botsPlayCounter); } - } - catch + else if (state == ClientState.Connecting) + { + Interlocked.Decrement(ref _botsConnectionCounter); + } + else if (state == ClientState.HandShake) + { + Interlocked.Decrement(ref _botsHandshakeCounter); + } + else if (state == ClientState.Login) + { + Interlocked.Decrement(ref _botsLoginCounter); + } + + //Console.WriteLine(ex.GetType().Name); + //Console.WriteLine(ex.Message); + //Console.WriteLine(ex.StackTrace); + + var key = Tuple.Create(exc.GetType().FullName, exc.Message); + + if (ExceptionCounter.TryGetValue(key, out var counter)) + { + counter.Increment(); + } + else + { + ExceptionCounter[key] = new ExceptionCounter(); + } + }; + + b.Client.OnStateChanged += onState; + b.Client.OnErrored += onError; + + _disposables.Add(Disposable.Create(() => { + b.Client.OnStateChanged -= onState; + b.Client.OnErrored -= onError; + })); + + + + stressTestBots.Add(b); + bot.DisposeWith(_disposables); + + } + + + + var metricsThread = new Thread(() => + { + Stopwatch stopwatch = new(); + + while (!cancellationTokenSource.IsCancellationRequested) + { + stopwatch.Start(); + var cps = Interlocked.Exchange(ref _cpsCounter, 0); + + var botsOnline = Volatile.Read(ref _botsPlayCounter); + + _dataPerSecond.OnNext(new StressTestMetrik(cps, botsOnline)); + + + stopwatch.Stop(); + if (stopwatch.Elapsed.Microseconds < 1000) + { + Thread.Sleep(1000 - stopwatch.Elapsed.Microseconds); + } + stopwatch.Reset(); } }) { Name = "Stress test counter", - IsBackground = true - }.Start(); + }; + + CompositeDisposable disposables = new(); _disposables.Add(disposables); @@ -243,42 +368,27 @@ public async Task Start(Serilog.ILogger logger) _cleanUp = _disposables; - + logger.Information("Запуск поведения"); if (Behavior is not null) { - logger.Information("Загружено поведение: " + Behavior.GetType().FullName); await Behavior.Activate(disposables, stressTestBots, cancellationTokenSource.Token); } - else - { - DefaultBehavior testBehavior = new(); - await testBehavior.Activate(disposables, stressTestBots, cancellationTokenSource.Token); - } + logger.Information("Поведение запущено"); + + metricsThread.Start(); + + logger.Information("Запущены потоки чтения метрик"); + CurrentState = StressTestServiceState.Running; } - catch + catch (Exception ex) { + logger.Error(ex, "Не удалось запустить стресс тест"); + CurrentState = StressTestServiceState.None; } } - private void Bot_StateChanged(object? sender, McProtoNet.StateChangedEventArgs e) - { - //Console.WriteLine(e.NewState); - if (e.NewState == ClientState.Play) - { - //Console.WriteLine("Play"); - Interlocked.Increment(ref _botsOnlineCounter); - Interlocked.Increment(ref _cpsCounter); - } - else if (e.NewState == ClientState.Failed) - { - if (e.OldState == ClientState.Play) - { - Interlocked.Decrement(ref _botsOnlineCounter); - } - } - } private async Task LoadProxy(Serilog.ILogger logger) @@ -289,30 +399,31 @@ private void Bot_StateChanged(object? sender, McProtoNet.StateChangedEventArgs e return null; } - logger.Information("Загрузка прокси"); - var sources = this.Proxies.Items.ToList(); + var sources = this.Proxies.Items.ToList(); + logger.Information("Загрузка прокси"); if (sources.Count() == 0) { sources.Add(new UrlProxySource(QuickProxyNet.ProxyType.HTTP, "https://raw.githubusercontent.com/TheSpeedX/PROXY-List/master/http.txt")); sources.Add(new UrlProxySource(QuickProxyNet.ProxyType.SOCKS4, "https://raw.githubusercontent.com/TheSpeedX/PROXY-List/master/socks4.txt")); sources.Add(new UrlProxySource(QuickProxyNet.ProxyType.SOCKS5, "https://raw.githubusercontent.com/TheSpeedX/PROXY-List/master/socks5.txt")); } + List>> tasks = new(); foreach (var s in sources) { tasks.Add(s.GetProxiesAsync()); } - + var result = await Task.WhenAll(tasks); var proxies = result.SelectMany(x => x).ToList(); - + var provider = new ProxyProvider(proxies); var group = proxies.GroupBy(x => x.Type).Select(x => $"{x.Key} - {x.Count()}"); - + logger.Information($"Загружено {proxies.Count} прокси. {string.Join(", ", group)}"); return provider; diff --git a/src/CoreLibs/HolyClient.StressTest/TidePVPBehaviorAttack.cs b/src/CoreLibs/HolyClient.StressTest/TidePVPBehaviorAttack.cs new file mode 100644 index 00000000..459d1d14 --- /dev/null +++ b/src/CoreLibs/HolyClient.StressTest/TidePVPBehaviorAttack.cs @@ -0,0 +1,14 @@ +using HolyClient.Abstractions.StressTest; +using System.Reactive.Disposables; + +namespace HolyClient.StressTest +{ + public class TidePVPBehaviorAttack : IStressTestBehavior + { + public Task Activate(CompositeDisposable disposables, IEnumerable bots, CancellationToken cancellationToken) + { + return Task.CompletedTask; + } + } + +} diff --git a/src/HolyClient/App.axaml.cs b/src/HolyClient/App.axaml.cs index ec8e7813..14d453dc 100644 --- a/src/HolyClient/App.axaml.cs +++ b/src/HolyClient/App.axaml.cs @@ -6,10 +6,14 @@ using Avalonia.Styling; using HolyClient.AppState; using HolyClient.Localization; +using HolyClient.Models; using HolyClient.ViewModels; using HolyClient.Views; +using ReactiveUI; using Splat; using System; +using System.Reflection; +using System.Threading; [assembly: XmlnsDefinition("https://github.com/avaloniaui", "HolyClient.Assets.Fonts.Roboto")] [assembly: XmlnsDefinition("https://github.com/avaloniaui", "HolyClient.Localization")] @@ -35,28 +39,34 @@ public override void Initialize() public override void OnFrameworkInitializationCompleted() { + ThreadPool.GetMinThreads(out var min, out var cpt); + ThreadPool.SetMinThreads(1, cpt); - MainViewModel mainViewModel = new(); - Locator.CurrentMutable.RegisterConstant(mainViewModel); + + + + RootViewModel root = new(); + Locator.CurrentMutable.RegisterConstant(root, "Root"); + + Locator.CurrentMutable.RegisterViewsForViewModels(Assembly.GetExecutingAssembly()); + + try { - MainView view = new MainView + + + RootView rootView = new() { - DataContext = mainViewModel + DataContext = root }; - Locator.CurrentMutable.RegisterConstant(view); + if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) { var wnd = new MainWindow() { - Content = view - }; - wnd.Opened += (s, e) => - { - var state = Locator.Current.GetService(); - mainViewModel.OnLoadState(state); - }; + Content = rootView + }; desktop.MainWindow = wnd; } @@ -72,7 +82,7 @@ public override void OnFrameworkInitializationCompleted() } catch (Exception e) { - Console.WriteLine(e); + if (ApplicationLifetime is ISingleViewApplicationLifetime single) { single.MainView = new TextBlock() diff --git a/src/HolyClient/AppState/BehaviorKey.cs b/src/HolyClient/AppState/BehaviorKey.cs new file mode 100644 index 00000000..d3b02dd4 --- /dev/null +++ b/src/HolyClient/AppState/BehaviorKey.cs @@ -0,0 +1,40 @@ +using MessagePack; +using System; + +namespace HolyClient.AppState; + +[MessagePackObject] +public struct BehaviorKey +{ + [Key(0)] + public readonly string Name; + [Key(1)] + public readonly string Assembly; + + public BehaviorKey(string name, string assembly) + { + Name = name; + Assembly = assembly; + } + + + public bool Equals(BehaviorKey c) + => c is BehaviorKey + && (Name, Assembly) + == (c.Name, c.Assembly); + + public override bool Equals(object o) + => (o is BehaviorKey c) && Equals(c); + + public static bool operator ==(in BehaviorKey c1, in BehaviorKey c2) + => Equals(c1, c2); + + public static bool operator !=(in BehaviorKey c1, in BehaviorKey c2) + => !Equals(c1, c2); + + + + public override int GetHashCode() + => HashCode.Combine(Name, Assembly); + +} diff --git a/src/HolyClient/AppState/MainState.cs b/src/HolyClient/AppState/MainState.cs index 2fded914..0de2dc81 100644 --- a/src/HolyClient/AppState/MainState.cs +++ b/src/HolyClient/AppState/MainState.cs @@ -4,6 +4,7 @@ using MessagePack; using ReactiveUI; using ReactiveUI.Fody.Helpers; +using System.Collections; namespace HolyClient.AppState; @@ -18,19 +19,13 @@ public class MainState : ReactiveObject [Key(1)] public Page SelectedPage { get; set; } - [Reactive] - [Key(2)] - public IBotManager BotManagerState { get; set; } = new BotManager(); - + [Key(5)] + public StressTestState StressTest { get; set; } = new(); - [Reactive] - [Key(3)] - public IStressTest StressTestState { get; set; } = new HolyClient.StressTest.StressTest(); [Reactive] [Key(4)] public ExtensionManagerState ExtensionManagerState { get; set; } = new(); } - diff --git a/src/HolyClient/AppState/StressTestState.cs b/src/HolyClient/AppState/StressTestState.cs new file mode 100644 index 00000000..49a3dd5b --- /dev/null +++ b/src/HolyClient/AppState/StressTestState.cs @@ -0,0 +1,33 @@ +using DynamicData; +using HolyClient.Core.Infrastructure; +using HolyClient.StressTest; +using MessagePack; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace HolyClient.AppState; + +[MessagePackObject(keyAsPropertyName: true)] +public sealed class StressTestState +{ + [IgnoreMember] + public SourceCache Profiles { get; } = new(x => x.Id); + + public Guid SelectedProfileId { get; set; } + + public IEnumerable ProfilesStates + { + get => Profiles.Items.ToList(); + set => Profiles.AddOrUpdate(value); + } + + internal async Task Initialization(IPluginProvider? pluginProvider) + { + foreach (var p in ProfilesStates) + { + await p.Initialization(pluginProvider); + } + } +} diff --git a/src/HolyClient/BootStrap.cs b/src/HolyClient/BootStrap.cs index ca84e8bd..2517e5ab 100644 --- a/src/HolyClient/BootStrap.cs +++ b/src/HolyClient/BootStrap.cs @@ -47,6 +47,11 @@ await Task.Run(async () => var state = RxApp.SuspensionHost.GetAppState(); + + + + + Loc.Instance.CurrentLanguage = state.SettingsState.Language; progress.OnNext("Bootstrap.LoadingState.LoadPlugins"); @@ -67,11 +72,11 @@ await Task.Run(async () => Locator.CurrentMutable.RegisterConstant(new PluginProvider()); - await state.BotManagerState.Initialization(); - await state.StressTestState.Initialization(Locator.Current.GetService()); + + await state.StressTest.Initialization(Locator.Current.GetService()); progress.OnNext("Bootstrap.LoadingState.AlmostDone"); @@ -124,12 +129,23 @@ await Dispatcher.UIThread.InvokeAsync(() => progress.OnCompleted(); }); + + var mainState = Locator.Current.GetService(); + + var mainViewModel = new MainViewModel(mainState); + + Locator.CurrentMutable.RegisterConstant(mainViewModel, "Main"); + Locator.CurrentMutable.RegisterConstant(mainViewModel); + + var root = Locator.Current.GetService("Root"); + + await root.Router.Navigate.Execute(mainViewModel); } private static void RegisterViewModels() { - Locator.CurrentMutable.RegisterViewsForViewModels(Assembly.GetExecutingAssembly()); + } @@ -137,16 +153,14 @@ private static void RegisterViewModels() private static void RegisterStates(MainState state) { Locator.CurrentMutable.RegisterConstant(state); - Locator.CurrentMutable.RegisterConstant(state.BotManagerState); Locator.CurrentMutable.RegisterConstant(state.SettingsState); - Locator.CurrentMutable.RegisterConstant(state.StressTestState); + Locator.CurrentMutable.RegisterConstant(state.StressTest); } private static void RegisterPages() { Locator.CurrentMutable.RegisterConstant(new HomeViewModel(), nameof(Page.Home)); - Locator.CurrentMutable.RegisterConstant(new BotManagerViewModel(), nameof(Page.BotManager)); Locator.CurrentMutable.RegisterConstant(new SettingsViewModel(), nameof(Page.Settings)); Locator.CurrentMutable.RegisterConstant(new StressTestViewModel(), nameof(Page.StressTest)); Locator.CurrentMutable.RegisterConstant(new ManagingExtensionsViewModel(), nameof(Page.ManagingExtensions)); diff --git a/src/HolyClient/Commands/StartStopBotCommand.cs b/src/HolyClient/Commands/StartStopBotCommand.cs deleted file mode 100644 index 0efd1548..00000000 --- a/src/HolyClient/Commands/StartStopBotCommand.cs +++ /dev/null @@ -1,50 +0,0 @@ -using HolyClient.Contracts.Models; -using ReactiveUI; -using ReactiveUI.Fody.Helpers; -using System; -using System.Windows.Input; - -namespace HolyClient.Commands -{ - public class StartStopBotCommand : ReactiveObject, ICommand - { - - private IBotProfile _profile; - private Serilog.ILogger _logger; - [Reactive] - public bool IsActivate { get; private set; } = false; - - - public StartStopBotCommand(IBotProfile profile, Serilog.ILogger logger) - { - _logger = logger; - _profile = profile; - } - private bool _canExectute; - - public event EventHandler? CanExecuteChanged; - - private void RaiseCanExecute() - { - - } - - public bool CanExecute(object? parameter) - { - return true; - } - - public void Execute(object? parameter) - { - if (IsActivate) - { - _profile.Stop(); - } - else - { - _profile.Start(this._logger); - } - IsActivate = !IsActivate; - } - } -} diff --git a/src/HolyClient/Commands/StartStressTestCommand.cs b/src/HolyClient/Commands/StartStressTestCommand.cs deleted file mode 100644 index adad22ac..00000000 --- a/src/HolyClient/Commands/StartStressTestCommand.cs +++ /dev/null @@ -1,189 +0,0 @@ -using HolyClient.StressTest; -using HolyClient.ViewModels; -using ReactiveUI; -using Serilog; -using System; -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Linq; -using System.Reactive.Disposables; -using System.Reactive.Linq; -using System.Threading; -using System.Threading.Tasks; -using System.Windows.Input; - -namespace HolyClient.Commands -{ - public class StartStressTestCommand : ICommand, IDisposable - { - public event EventHandler? CanExecuteChanged; - - private IScreen screen; - private IStressTest _model; - private IDisposable? _cleanUp = null; - private bool _canExecute; - public StartStressTestCommand(IScreen screen, IStressTest model, IObservable canExecute) - { - CompositeDisposable d = new(); - - this.screen = screen; - _model = model; - - canExecute.Subscribe(x => - { - Console.WriteLine("CanExecute: " + _canExecute); - _canExecute = x; - this.CanExecuteChanged?.Invoke(this, new EventArgs()); - }).DisposeWith(d); - - _cleanUp = d; - } - - public bool CanExecute(object? parameter) - { - return _canExecute; - } - public async void Execute(object? parameter) - { - if (!_canExecute) - return; - - Thread.CurrentThread.Priority = ThreadPriority.Highest; - LoggerWrapper loggerWrapper = new LoggerWrapper(); - - ILogger logger = loggerWrapper; - - try - { - - - - StressTestLoadingViewModel loadingVM = new StressTestLoadingViewModel(this.screen, _model); - - StressTestProcessViewModel proccess = new StressTestProcessViewModel(this.screen, _model, loggerWrapper); - await screen.Router.Navigate.Execute(loadingVM); - - - - await _model.Start(logger); - //await Task.Factory.StartNew(() =>, default, TaskCreationOptions.LongRunning, StaScheduler).Unwrap(); - - - await screen.Router.Navigate.Execute(proccess); - - } - catch (TaskCanceledException) - { - logger.Information("[STRESS TEST] Завершился из-за отмены"); - } - catch (Exception ex) - { - logger.Error(ex, "[STRESS TEST] завершился с ошибкой"); - } - finally - { - - } - - } - - public void Dispose() - { - Interlocked.Exchange(ref _cleanUp, null)?.Dispose(); - } - } - public sealed class StaTaskScheduler : TaskScheduler, IDisposable - { - /// Stores the queued tasks to be executed by our pool of STA threads. - private BlockingCollection _tasks; - /// The STA threads used by the scheduler. - private readonly List _threads; - - /// Initializes a new instance of the StaTaskScheduler class with the specified concurrency level. - /// The number of threads that should be created and used by this scheduler. - public StaTaskScheduler(int numberOfThreads) - { - // Validate arguments - if (numberOfThreads < 1) throw new ArgumentOutOfRangeException("concurrencyLevel"); - - // Initialize the tasks collection - _tasks = new BlockingCollection(); - - // Create the threads to be used by this scheduler - _threads = Enumerable.Range(0, numberOfThreads).Select(i => - { - var thread = new Thread(() => - { - // Continually get the next task and try to execute it. - // This will continue until the scheduler is disposed and no more tasks remain. - foreach (var t in _tasks.GetConsumingEnumerable()) - { - TryExecuteTask(t); - } - }); - thread.Name = "STA THREAD"; - thread.IsBackground = true; - // thread.SetApartmentState(ApartmentState.STA); - return thread; - }).ToList(); - - // Start all of the threads - _threads.ForEach(t => t.Start()); - } - - /// Queues a Task to be executed by this scheduler. - /// The task to be executed. - protected override void QueueTask(Task task) - { - // Push it into the blocking collection of tasks - _tasks.Add(task); - } - - /// Provides a list of the scheduled tasks for the debugger to consume. - /// An enumerable of all tasks currently scheduled. - protected override IEnumerable GetScheduledTasks() - { - // Serialize the contents of the blocking collection of tasks for the debugger - return _tasks.ToArray(); - } - - /// Determines whether a Task may be inlined. - /// The task to be executed. - /// Whether the task was previously queued. - /// true if the task was successfully inlined; otherwise, false. - protected override bool TryExecuteTaskInline(Task task, bool taskWasPreviouslyQueued) - { - // Try to inline if the current thread is STA - return - Thread.CurrentThread.GetApartmentState() == ApartmentState.STA && - TryExecuteTask(task); - } - - /// Gets the maximum concurrency level supported by this scheduler. - public override int MaximumConcurrencyLevel - { - get { return _threads.Count; } - } - - /// - /// Cleans up the scheduler by indicating that no more tasks will be queued. - /// This method blocks until all threads successfully shutdown. - /// - public void Dispose() - { - if (_tasks != null) - { - // Indicate that no new tasks will be coming in - _tasks.CompleteAdding(); - - // Wait for all threads to finish processing tasks - foreach (var thread in _threads) thread.Join(); - - // Cleanup - _tasks.Dispose(); - _tasks = null; - } - } - } - -} diff --git a/src/HolyClient/Commands/StopStressTestCommand.cs b/src/HolyClient/Commands/StopStressTestCommand.cs deleted file mode 100644 index f3a8bba0..00000000 --- a/src/HolyClient/Commands/StopStressTestCommand.cs +++ /dev/null @@ -1,32 +0,0 @@ -using HolyClient.StressTest; -using HolyClient.ViewModels; -using ReactiveUI; -using System; -using System.Reactive.Linq; -using System.Windows.Input; - -namespace HolyClient.Commands -{ - public class StopStressTestCommand : ICommand - { - public event EventHandler? CanExecuteChanged; - private readonly IScreen screen; - private readonly IStressTest stressTest; - public StopStressTestCommand(IScreen screen, IStressTest stressTest) - { - this.screen = screen; - this.stressTest = stressTest; - } - - public bool CanExecute(object? parameter) - { - return true; - } - - public async void Execute(object? parameter) - { - await this.stressTest.Stop(); - await screen.Router.NavigateAndReset.Execute(new StressTestConfigurationViewModel(screen, stressTest)); - } - } -} diff --git a/src/HolyClient/DesignTime/DesignBotManagerViewModel.cs b/src/HolyClient/DesignTime/DesignBotManagerViewModel.cs deleted file mode 100644 index 67656add..00000000 --- a/src/HolyClient/DesignTime/DesignBotManagerViewModel.cs +++ /dev/null @@ -1,59 +0,0 @@ -using HolyClient.ViewModels; -using ReactiveUI; -using System; -using System.Collections.ObjectModel; -using System.ComponentModel; -using System.Linq; -using System.Reactive; - -namespace HolyClient.DesignTime -{ - public class DesignBotManagerViewModel : IBotManagerViewModel - { - public DesignBotManagerViewModel() - { - ObservableCollection profiles = new() - { - new DesignBotProfileViewModel() - { - Name = "New Profile" - } - }; - Profiles = new(profiles); - SelectedProfile = Profiles.First(); - - } - - public ViewModelActivator Activator { get; } = new(); - - public ReactiveCommand CreateProfileCommand { get; } - - public IScreen HostScreen => null; - - public ReadOnlyObservableCollection Profiles { get; } - - public RoutingState Router { get; set; } - - - public IBotProfileViewModel SelectedProfile { get; set; } - - public string? UrlPathSegment => "botManager"; - - public Interaction Dialog => throw new NotImplementedException(); - - public ReactiveCommand RemoveProfileCommand => throw new NotImplementedException(); - - public event PropertyChangedEventHandler? PropertyChanged; - public event PropertyChangingEventHandler? PropertyChanging; - - public void RaisePropertyChanged(PropertyChangedEventArgs args) - { - throw new NotImplementedException(); - } - - public void RaisePropertyChanging(PropertyChangingEventArgs args) - { - throw new NotImplementedException(); - } - } -} diff --git a/src/HolyClient/DesignTime/DesignBottingProfileViewModel.cs b/src/HolyClient/DesignTime/DesignBottingProfileViewModel.cs deleted file mode 100644 index 3f39d06f..00000000 --- a/src/HolyClient/DesignTime/DesignBottingProfileViewModel.cs +++ /dev/null @@ -1,40 +0,0 @@ -using HolyClient.ViewModels; -using McProtoNet; -using ReactiveUI; -using System; -using System.Collections.ObjectModel; -using System.Windows.Input; - -namespace HolyClient.DesignTime -{ - public class DesignBotProfileViewModel : IBotProfileViewModel - { - public ViewModelActivator Activator { get; } - - public Guid Id => Guid.NewGuid(); - - public string Name { get; set; } - - public ICommand StartBotCommand => throw new NotImplementedException(); - - public ICommand StopBotCommand => throw new NotImplementedException(); - - public RoutingState Router => throw new NotImplementedException(); - - - - public ObservableCollection LogItems => throw new NotImplementedException(); - - public ConsoleViewModel Console => throw new NotImplementedException(); - - public string Server { get => throw new NotImplementedException(); set => throw new NotImplementedException(); } - public string Nickname { get => throw new NotImplementedException(); set => throw new NotImplementedException(); } - public MinecraftVersion Version { get => throw new NotImplementedException(); set => throw new NotImplementedException(); } - public int SelectedTab { get => throw new NotImplementedException(); set => throw new NotImplementedException(); } - - public void Dispose() - { - - } - } -} diff --git a/src/HolyClient/Designer/CustomStyles.axaml b/src/HolyClient/Designer/CustomStyles.axaml index 5138b575..cf7ecc58 100644 --- a/src/HolyClient/Designer/CustomStyles.axaml +++ b/src/HolyClient/Designer/CustomStyles.axaml @@ -12,7 +12,7 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/HolyClient/Views/Pages/StressTest/StressTestView.axaml.cs b/src/HolyClient/Views/Pages/StressTest/StressTestView.axaml.cs index e175a5c0..f9a43fc8 100644 --- a/src/HolyClient/Views/Pages/StressTest/StressTestView.axaml.cs +++ b/src/HolyClient/Views/Pages/StressTest/StressTestView.axaml.cs @@ -1,5 +1,8 @@ using Avalonia.ReactiveUI; +using FluentAvalonia.UI.Controls; +using HolyClient.Localization; using HolyClient.ViewModels; +using ReactiveUI; namespace HolyClient.Views; @@ -8,5 +11,32 @@ public partial class StressTestView : ReactiveUserControl public StressTestView() { InitializeComponent(); + + this.WhenActivated(d => + { + + + + + + this.ViewModel.ConfirmRemoveDialog.RegisterHandler(async x => + { + ContentDialog dialog = new ContentDialog() + { + Title = "Вы точно хотите удалить?", + PrimaryButtonText = Loc.Tr("Yes"), + IsSecondaryButtonEnabled = false, + CloseButtonText = Loc.Tr("No") + + }; + + var result = await dialog.ShowAsync(); + + x.SetOutput(result == ContentDialogResult.Primary); + }); + + + + }); } } \ No newline at end of file diff --git a/src/HolyClient/Views/Pages/BotManager/EmptyProfilesView.axaml b/src/HolyClient/Views/RootView.axaml similarity index 53% rename from src/HolyClient/Views/Pages/BotManager/EmptyProfilesView.axaml rename to src/HolyClient/Views/RootView.axaml index 10ef412e..5c3fa2cc 100644 --- a/src/HolyClient/Views/Pages/BotManager/EmptyProfilesView.axaml +++ b/src/HolyClient/Views/RootView.axaml @@ -1,12 +1,9 @@ - - - -