mirror of
https://github.com/bloxstraplabs/bloxstrap.git
synced 2025-04-21 10:01:27 -07:00
Yep
This commit is contained in:
parent
552f2a52a6
commit
8e47290e3d
@ -1,32 +0,0 @@
|
|||||||
|
|
||||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
|
||||||
# Visual Studio Version 17
|
|
||||||
VisualStudioVersion = 17.3.32819.101
|
|
||||||
MinimumVisualStudioVersion = 10.0.40219.1
|
|
||||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Bloxstrap", "Bloxstrap\Bloxstrap.csproj", "{0D75146E-DA24-4B05-B6C9-250C8F81B0C7}"
|
|
||||||
EndProject
|
|
||||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Wpf.Ui", "wpfui\src\Wpf.Ui\Wpf.Ui.csproj", "{1ADC87D1-8963-4100-845A-18477824718E}"
|
|
||||||
EndProject
|
|
||||||
Global
|
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
|
||||||
Debug|Any CPU = Debug|Any CPU
|
|
||||||
Release|Any CPU = Release|Any CPU
|
|
||||||
EndGlobalSection
|
|
||||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
|
||||||
{0D75146E-DA24-4B05-B6C9-250C8F81B0C7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
|
||||||
{0D75146E-DA24-4B05-B6C9-250C8F81B0C7}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
|
||||||
{0D75146E-DA24-4B05-B6C9-250C8F81B0C7}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
|
||||||
{0D75146E-DA24-4B05-B6C9-250C8F81B0C7}.Release|Any CPU.Build.0 = Release|Any CPU
|
|
||||||
{1ADC87D1-8963-4100-845A-18477824718E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
|
||||||
{1ADC87D1-8963-4100-845A-18477824718E}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
|
||||||
{1ADC87D1-8963-4100-845A-18477824718E}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
|
||||||
{1ADC87D1-8963-4100-845A-18477824718E}.Release|Any CPU.Build.0 = Release|Any CPU
|
|
||||||
EndGlobalSection
|
|
||||||
GlobalSection(SolutionProperties) = preSolution
|
|
||||||
HideSolutionNode = FALSE
|
|
||||||
EndGlobalSection
|
|
||||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
|
||||||
RESX_NeutralResourcesLanguage = en-GB
|
|
||||||
SolutionGuid = {ED269E5D-8C72-49B4-A76F-51CF163511C1}
|
|
||||||
EndGlobalSection
|
|
||||||
EndGlobal
|
|
1
Bloxstrap.txt
Normal file
1
Bloxstrap.txt
Normal file
@ -0,0 +1 @@
|
|||||||
|
IT WORKS
|
@ -1,42 +0,0 @@
|
|||||||
<Application x:Class="Bloxstrap.App"
|
|
||||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
|
||||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
|
||||||
xmlns:local="clr-namespace:Bloxstrap"
|
|
||||||
xmlns:ui="http://schemas.lepo.co/wpfui/2022/xaml"
|
|
||||||
xmlns:converters="clr-namespace:Bloxstrap.UI.Converters"
|
|
||||||
ShutdownMode="OnExplicitShutdown"
|
|
||||||
DispatcherUnhandledException="GlobalExceptionHandler">
|
|
||||||
<Application.Resources>
|
|
||||||
<ResourceDictionary>
|
|
||||||
<ResourceDictionary.MergedDictionaries>
|
|
||||||
<ui:ThemesDictionary Theme="Dark" />
|
|
||||||
<ui:ControlsDictionary />
|
|
||||||
</ResourceDictionary.MergedDictionaries>
|
|
||||||
|
|
||||||
<FontFamily x:Key="Rubik">pack://application:,,,/Resources/Fonts/#Rubik Light</FontFamily>
|
|
||||||
|
|
||||||
<Style TargetType="Hyperlink">
|
|
||||||
<Style.Triggers>
|
|
||||||
<Trigger Property="IsMouseOver" Value="True">
|
|
||||||
<Setter Property="Foreground">
|
|
||||||
<Setter.Value>
|
|
||||||
<SolidColorBrush Color="{DynamicResource SystemAccentColorTertiary}" />
|
|
||||||
</Setter.Value>
|
|
||||||
</Setter>
|
|
||||||
<Setter Property="TextDecorations" Value="Underline" />
|
|
||||||
</Trigger>
|
|
||||||
</Style.Triggers>
|
|
||||||
<Setter Property="TextDecorations" Value="None" />
|
|
||||||
<Setter Property="Foreground">
|
|
||||||
<Setter.Value>
|
|
||||||
<SolidColorBrush Color="{DynamicResource SystemAccentColorSecondary}" />
|
|
||||||
</Setter.Value>
|
|
||||||
</Setter>
|
|
||||||
</Style>
|
|
||||||
|
|
||||||
<converters:StringFormatConverter x:Key="StringFormatConverter" />
|
|
||||||
<converters:RangeConverter x:Key="RangeConverter" />
|
|
||||||
<converters:EnumNameConverter x:Key="EnumNameConverter" />
|
|
||||||
</ResourceDictionary>
|
|
||||||
</Application.Resources>
|
|
||||||
</Application>
|
|
@ -1,359 +0,0 @@
|
|||||||
using System.Reflection;
|
|
||||||
using System.Security.Cryptography;
|
|
||||||
using System.Windows;
|
|
||||||
using System.Windows.Shell;
|
|
||||||
using System.Windows.Threading;
|
|
||||||
|
|
||||||
using Microsoft.Win32;
|
|
||||||
|
|
||||||
namespace Bloxstrap
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Interaction logic for App.xaml
|
|
||||||
/// </summary>
|
|
||||||
public partial class App : Application
|
|
||||||
{
|
|
||||||
#if QA_BUILD
|
|
||||||
public const string ProjectName = "Bloxstrap-QA";
|
|
||||||
#else
|
|
||||||
public const string ProjectName = "Bloxstrap";
|
|
||||||
#endif
|
|
||||||
public const string ProjectOwner = "Bloxstrap";
|
|
||||||
public const string ProjectRepository = "bloxstraplabs/bloxstrap";
|
|
||||||
public const string ProjectDownloadLink = "https://bloxstraplabs.com";
|
|
||||||
public const string ProjectHelpLink = "https://github.com/bloxstraplabs/bloxstrap/wiki";
|
|
||||||
public const string ProjectSupportLink = "https://github.com/bloxstraplabs/bloxstrap/issues/new";
|
|
||||||
|
|
||||||
public const string RobloxPlayerAppName = "RobloxPlayerBeta";
|
|
||||||
public const string RobloxStudioAppName = "RobloxStudioBeta";
|
|
||||||
|
|
||||||
// simple shorthand for extremely frequently used and long string - this goes under HKCU
|
|
||||||
public const string UninstallKey = $@"Software\Microsoft\Windows\CurrentVersion\Uninstall\{ProjectName}";
|
|
||||||
|
|
||||||
public static LaunchSettings LaunchSettings { get; private set; } = null!;
|
|
||||||
|
|
||||||
public static BuildMetadataAttribute BuildMetadata = Assembly.GetExecutingAssembly().GetCustomAttribute<BuildMetadataAttribute>()!;
|
|
||||||
|
|
||||||
public static string Version = Assembly.GetExecutingAssembly().GetName().Version!.ToString()[..^2];
|
|
||||||
|
|
||||||
public static Bootstrapper? Bootstrapper { get; set; } = null!;
|
|
||||||
|
|
||||||
public static bool IsActionBuild => !String.IsNullOrEmpty(BuildMetadata.CommitRef);
|
|
||||||
|
|
||||||
public static bool IsProductionBuild => IsActionBuild && BuildMetadata.CommitRef.StartsWith("tag", StringComparison.Ordinal);
|
|
||||||
|
|
||||||
public static bool IsStudioVisible => !String.IsNullOrEmpty(App.State.Prop.Studio.VersionGuid);
|
|
||||||
|
|
||||||
public static readonly MD5 MD5Provider = MD5.Create();
|
|
||||||
|
|
||||||
public static readonly Logger Logger = new();
|
|
||||||
|
|
||||||
public static readonly Dictionary<string, BaseTask> PendingSettingTasks = new();
|
|
||||||
|
|
||||||
public static readonly JsonManager<Settings> Settings = new();
|
|
||||||
|
|
||||||
public static readonly JsonManager<State> State = new();
|
|
||||||
|
|
||||||
public static readonly FastFlagManager FastFlags = new();
|
|
||||||
|
|
||||||
public static readonly HttpClient HttpClient = new(
|
|
||||||
new HttpClientLoggingHandler(
|
|
||||||
new HttpClientHandler { AutomaticDecompression = DecompressionMethods.All }
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
private static bool _showingExceptionDialog = false;
|
|
||||||
|
|
||||||
public static void Terminate(ErrorCode exitCode = ErrorCode.ERROR_SUCCESS)
|
|
||||||
{
|
|
||||||
int exitCodeNum = (int)exitCode;
|
|
||||||
|
|
||||||
Logger.WriteLine("App::Terminate", $"Terminating with exit code {exitCodeNum} ({exitCode})");
|
|
||||||
|
|
||||||
Environment.Exit(exitCodeNum);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void SoftTerminate(ErrorCode exitCode = ErrorCode.ERROR_SUCCESS)
|
|
||||||
{
|
|
||||||
int exitCodeNum = (int)exitCode;
|
|
||||||
|
|
||||||
Logger.WriteLine("App::SoftTerminate", $"Terminating with exit code {exitCodeNum} ({exitCode})");
|
|
||||||
|
|
||||||
Current.Dispatcher.Invoke(() => Current.Shutdown(exitCodeNum));
|
|
||||||
}
|
|
||||||
|
|
||||||
void GlobalExceptionHandler(object sender, DispatcherUnhandledExceptionEventArgs e)
|
|
||||||
{
|
|
||||||
e.Handled = true;
|
|
||||||
|
|
||||||
Logger.WriteLine("App::GlobalExceptionHandler", "An exception occurred");
|
|
||||||
|
|
||||||
FinalizeExceptionHandling(e.Exception);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void FinalizeExceptionHandling(AggregateException ex)
|
|
||||||
{
|
|
||||||
foreach (var innerEx in ex.InnerExceptions)
|
|
||||||
Logger.WriteException("App::FinalizeExceptionHandling", innerEx);
|
|
||||||
|
|
||||||
FinalizeExceptionHandling(ex.GetBaseException(), false);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void FinalizeExceptionHandling(Exception ex, bool log = true)
|
|
||||||
{
|
|
||||||
if (log)
|
|
||||||
Logger.WriteException("App::FinalizeExceptionHandling", ex);
|
|
||||||
|
|
||||||
if (_showingExceptionDialog)
|
|
||||||
return;
|
|
||||||
|
|
||||||
_showingExceptionDialog = true;
|
|
||||||
|
|
||||||
SendLog();
|
|
||||||
|
|
||||||
if (Bootstrapper?.Dialog != null)
|
|
||||||
{
|
|
||||||
if (Bootstrapper.Dialog.TaskbarProgressValue == 0)
|
|
||||||
Bootstrapper.Dialog.TaskbarProgressValue = 1; // make sure it's visible
|
|
||||||
|
|
||||||
Bootstrapper.Dialog.TaskbarProgressState = TaskbarItemProgressState.Error;
|
|
||||||
}
|
|
||||||
|
|
||||||
Frontend.ShowExceptionDialog(ex);
|
|
||||||
|
|
||||||
Terminate(ErrorCode.ERROR_INSTALL_FAILURE);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static async Task<GithubRelease?> GetLatestRelease()
|
|
||||||
{
|
|
||||||
const string LOG_IDENT = "App::GetLatestRelease";
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var releaseInfo = await Http.GetJson<GithubRelease>($"https://api.github.com/repos/{ProjectRepository}/releases/latest");
|
|
||||||
|
|
||||||
if (releaseInfo is null || releaseInfo.Assets is null)
|
|
||||||
{
|
|
||||||
Logger.WriteLine(LOG_IDENT, "Encountered invalid data");
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return releaseInfo;
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
Logger.WriteException(LOG_IDENT, ex);
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static async void SendStat(string key, string value)
|
|
||||||
{
|
|
||||||
if (!Settings.Prop.EnableAnalytics)
|
|
||||||
return;
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
await HttpClient.GetAsync($"https://bloxstraplabs.com/metrics/post?key={key}&value={value}");
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
Logger.WriteException("App::SendStat", ex);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static async void SendLog()
|
|
||||||
{
|
|
||||||
if (!Settings.Prop.EnableAnalytics || !IsProductionBuild)
|
|
||||||
return;
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
await HttpClient.PostAsync(
|
|
||||||
$"https://bloxstraplabs.com/metrics/post-exception",
|
|
||||||
new StringContent(Logger.AsDocument)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
Logger.WriteException("App::SendLog", ex);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void AssertWindowsOSVersion()
|
|
||||||
{
|
|
||||||
const string LOG_IDENT = "App::AssertWindowsOSVersion";
|
|
||||||
|
|
||||||
int major = Environment.OSVersion.Version.Major;
|
|
||||||
if (major < 10) // Windows 10 and newer only
|
|
||||||
{
|
|
||||||
Logger.WriteLine(LOG_IDENT, $"Detected unsupported Windows version ({Environment.OSVersion.Version}).");
|
|
||||||
|
|
||||||
if (!LaunchSettings.QuietFlag.Active)
|
|
||||||
Frontend.ShowMessageBox(Strings.App_OSDeprecation_Win7_81, MessageBoxImage.Error);
|
|
||||||
|
|
||||||
Terminate(ErrorCode.ERROR_INVALID_FUNCTION);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override void OnStartup(StartupEventArgs e)
|
|
||||||
{
|
|
||||||
const string LOG_IDENT = "App::OnStartup";
|
|
||||||
|
|
||||||
Locale.Initialize();
|
|
||||||
|
|
||||||
base.OnStartup(e);
|
|
||||||
|
|
||||||
Logger.WriteLine(LOG_IDENT, $"Starting {ProjectName} v{Version}");
|
|
||||||
|
|
||||||
string userAgent = $"{ProjectName}/{Version}";
|
|
||||||
|
|
||||||
if (IsActionBuild)
|
|
||||||
{
|
|
||||||
Logger.WriteLine(LOG_IDENT, $"Compiled {BuildMetadata.Timestamp.ToFriendlyString()} from commit {BuildMetadata.CommitHash} ({BuildMetadata.CommitRef})");
|
|
||||||
|
|
||||||
if (IsProductionBuild)
|
|
||||||
userAgent += $" (Production)";
|
|
||||||
else
|
|
||||||
userAgent += $" (Artifact {BuildMetadata.CommitHash}, {BuildMetadata.CommitRef})";
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
Logger.WriteLine(LOG_IDENT, $"Compiled {BuildMetadata.Timestamp.ToFriendlyString()} from {BuildMetadata.Machine}");
|
|
||||||
|
|
||||||
#if QA_BUILD
|
|
||||||
userAgent += " (QA)";
|
|
||||||
#else
|
|
||||||
userAgent += $" (Build {Convert.ToBase64String(Encoding.UTF8.GetBytes(BuildMetadata.Machine))})";
|
|
||||||
#endif
|
|
||||||
}
|
|
||||||
|
|
||||||
Logger.WriteLine(LOG_IDENT, $"OSVersion: {Environment.OSVersion}");
|
|
||||||
|
|
||||||
Logger.WriteLine(LOG_IDENT, $"Loaded from {Paths.Process}");
|
|
||||||
Logger.WriteLine(LOG_IDENT, $"Temp path is {Paths.Temp}");
|
|
||||||
Logger.WriteLine(LOG_IDENT, $"WindowsStartMenu path is {Paths.WindowsStartMenu}");
|
|
||||||
|
|
||||||
// To customize application configuration such as set high DPI settings or default font,
|
|
||||||
// see https://aka.ms/applicationconfiguration.
|
|
||||||
ApplicationConfiguration.Initialize();
|
|
||||||
|
|
||||||
HttpClient.Timeout = TimeSpan.FromSeconds(30);
|
|
||||||
HttpClient.DefaultRequestHeaders.Add("User-Agent", userAgent);
|
|
||||||
|
|
||||||
LaunchSettings = new LaunchSettings(e.Args);
|
|
||||||
|
|
||||||
// installation check begins here
|
|
||||||
using var uninstallKey = Registry.CurrentUser.OpenSubKey(UninstallKey);
|
|
||||||
string? installLocation = null;
|
|
||||||
bool fixInstallLocation = false;
|
|
||||||
|
|
||||||
if (uninstallKey?.GetValue("InstallLocation") is string value)
|
|
||||||
{
|
|
||||||
if (Directory.Exists(value))
|
|
||||||
{
|
|
||||||
installLocation = value;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
// check if user profile folder has been renamed
|
|
||||||
var match = Regex.Match(value, @"^[a-zA-Z]:\\Users\\([^\\]+)", RegexOptions.IgnoreCase);
|
|
||||||
|
|
||||||
if (match.Success)
|
|
||||||
{
|
|
||||||
string newLocation = value.Replace(match.Value, Paths.UserProfile, StringComparison.InvariantCultureIgnoreCase);
|
|
||||||
|
|
||||||
if (Directory.Exists(newLocation))
|
|
||||||
{
|
|
||||||
installLocation = newLocation;
|
|
||||||
fixInstallLocation = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// silently change install location if we detect a portable run
|
|
||||||
if (installLocation is null && Directory.GetParent(Paths.Process)?.FullName is string processDir)
|
|
||||||
{
|
|
||||||
var files = Directory.GetFiles(processDir).Select(x => Path.GetFileName(x)).ToArray();
|
|
||||||
|
|
||||||
// check if settings.json and state.json are the only files in the folder
|
|
||||||
if (files.Length <= 3 && files.Contains("Settings.json") && files.Contains("State.json"))
|
|
||||||
{
|
|
||||||
installLocation = processDir;
|
|
||||||
fixInstallLocation = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (fixInstallLocation && installLocation is not null)
|
|
||||||
{
|
|
||||||
var installer = new Installer
|
|
||||||
{
|
|
||||||
InstallLocation = installLocation,
|
|
||||||
IsImplicitInstall = true
|
|
||||||
};
|
|
||||||
|
|
||||||
if (installer.CheckInstallLocation())
|
|
||||||
{
|
|
||||||
Logger.WriteLine(LOG_IDENT, $"Changing install location to '{installLocation}'");
|
|
||||||
installer.DoInstall();
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
// force reinstall
|
|
||||||
installLocation = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (installLocation is null)
|
|
||||||
{
|
|
||||||
Logger.Initialize(true);
|
|
||||||
Logger.WriteLine(LOG_IDENT, "Not installed, launching the installer");
|
|
||||||
AssertWindowsOSVersion(); // prevent new installs from unsupported operating systems
|
|
||||||
LaunchHandler.LaunchInstaller();
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
Paths.Initialize(installLocation);
|
|
||||||
|
|
||||||
Logger.WriteLine(LOG_IDENT, "Entering main logic");
|
|
||||||
|
|
||||||
// ensure executable is in the install directory
|
|
||||||
if (Paths.Process != Paths.Application && !File.Exists(Paths.Application))
|
|
||||||
{
|
|
||||||
Logger.WriteLine(LOG_IDENT, "Copying to install directory");
|
|
||||||
File.Copy(Paths.Process, Paths.Application);
|
|
||||||
}
|
|
||||||
|
|
||||||
Logger.Initialize(LaunchSettings.UninstallFlag.Active);
|
|
||||||
|
|
||||||
if (!Logger.Initialized && !Logger.NoWriteMode)
|
|
||||||
{
|
|
||||||
Logger.WriteLine(LOG_IDENT, "Possible duplicate launch detected, terminating.");
|
|
||||||
Terminate();
|
|
||||||
}
|
|
||||||
|
|
||||||
Settings.Load();
|
|
||||||
State.Load();
|
|
||||||
FastFlags.Load();
|
|
||||||
|
|
||||||
if (!Locale.SupportedLocales.ContainsKey(Settings.Prop.Locale))
|
|
||||||
{
|
|
||||||
Settings.Prop.Locale = "nil";
|
|
||||||
Settings.Save();
|
|
||||||
}
|
|
||||||
|
|
||||||
Locale.Set(Settings.Prop.Locale);
|
|
||||||
|
|
||||||
if (!LaunchSettings.BypassUpdateCheck)
|
|
||||||
Installer.HandleUpgrade();
|
|
||||||
|
|
||||||
LaunchHandler.ProcessLaunchArgs();
|
|
||||||
}
|
|
||||||
|
|
||||||
// you must *explicitly* call terminate when everything is done, it won't be called implicitly
|
|
||||||
Logger.WriteLine(LOG_IDENT, "Startup finished");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,74 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Text;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
namespace Bloxstrap.AppData
|
|
||||||
{
|
|
||||||
public abstract class CommonAppData
|
|
||||||
{
|
|
||||||
// in case a new package is added, you can find the corresponding directory
|
|
||||||
// by opening the stock bootstrapper in a hex editor
|
|
||||||
private IReadOnlyDictionary<string, string> _commonMap { get; } = new Dictionary<string, string>()
|
|
||||||
{
|
|
||||||
{ "Libraries.zip", @"" },
|
|
||||||
{ "redist.zip", @"" },
|
|
||||||
{ "shaders.zip", @"shaders\" },
|
|
||||||
{ "ssl.zip", @"ssl\" },
|
|
||||||
|
|
||||||
// the runtime installer is only extracted if it needs installing
|
|
||||||
{ "WebView2.zip", @"" },
|
|
||||||
{ "WebView2RuntimeInstaller.zip", @"WebView2RuntimeInstaller\" },
|
|
||||||
|
|
||||||
{ "content-avatar.zip", @"content\avatar\" },
|
|
||||||
{ "content-configs.zip", @"content\configs\" },
|
|
||||||
{ "content-fonts.zip", @"content\fonts\" },
|
|
||||||
{ "content-sky.zip", @"content\sky\" },
|
|
||||||
{ "content-sounds.zip", @"content\sounds\" },
|
|
||||||
{ "content-textures2.zip", @"content\textures\" },
|
|
||||||
{ "content-models.zip", @"content\models\" },
|
|
||||||
|
|
||||||
{ "content-textures3.zip", @"PlatformContent\pc\textures\" },
|
|
||||||
{ "content-terrain.zip", @"PlatformContent\pc\terrain\" },
|
|
||||||
{ "content-platform-fonts.zip", @"PlatformContent\pc\fonts\" },
|
|
||||||
{ "content-platform-dictionaries.zip", @"PlatformContent\pc\shared_compression_dictionaries\" },
|
|
||||||
|
|
||||||
{ "extracontent-luapackages.zip", @"ExtraContent\LuaPackages\" },
|
|
||||||
{ "extracontent-translations.zip", @"ExtraContent\translations\" },
|
|
||||||
{ "extracontent-models.zip", @"ExtraContent\models\" },
|
|
||||||
{ "extracontent-textures.zip", @"ExtraContent\textures\" },
|
|
||||||
{ "extracontent-places.zip", @"ExtraContent\places\" },
|
|
||||||
};
|
|
||||||
|
|
||||||
public virtual string ExecutableName { get; } = null!;
|
|
||||||
|
|
||||||
public string Directory => Path.Combine(Paths.Versions, State.VersionGuid);
|
|
||||||
|
|
||||||
public string ExecutablePath => Path.Combine(Directory, ExecutableName);
|
|
||||||
|
|
||||||
public virtual AppState State { get; } = null!;
|
|
||||||
|
|
||||||
public virtual IReadOnlyDictionary<string, string> PackageDirectoryMap { get; set; }
|
|
||||||
|
|
||||||
|
|
||||||
public CommonAppData()
|
|
||||||
{
|
|
||||||
if (PackageDirectoryMap is null)
|
|
||||||
{
|
|
||||||
PackageDirectoryMap = _commonMap;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var merged = new Dictionary<string, string>();
|
|
||||||
|
|
||||||
foreach (var entry in _commonMap)
|
|
||||||
merged[entry.Key] = entry.Value;
|
|
||||||
|
|
||||||
foreach (var entry in PackageDirectoryMap)
|
|
||||||
merged[entry.Key] = entry.Value;
|
|
||||||
|
|
||||||
PackageDirectoryMap = merged;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,21 +0,0 @@
|
|||||||
namespace Bloxstrap.AppData
|
|
||||||
{
|
|
||||||
internal interface IAppData
|
|
||||||
{
|
|
||||||
string ProductName { get; }
|
|
||||||
|
|
||||||
string BinaryType { get; }
|
|
||||||
|
|
||||||
string RegistryName { get; }
|
|
||||||
|
|
||||||
string ExecutableName { get; }
|
|
||||||
|
|
||||||
string Directory { get; }
|
|
||||||
|
|
||||||
string ExecutablePath { get; }
|
|
||||||
|
|
||||||
AppState State { get; }
|
|
||||||
|
|
||||||
IReadOnlyDictionary<string, string> PackageDirectoryMap { get; set; }
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,26 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Text;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
namespace Bloxstrap.AppData
|
|
||||||
{
|
|
||||||
public class RobloxPlayerData : CommonAppData, IAppData
|
|
||||||
{
|
|
||||||
public string ProductName => "Roblox";
|
|
||||||
|
|
||||||
public string BinaryType => "WindowsPlayer";
|
|
||||||
|
|
||||||
public string RegistryName => "RobloxPlayer";
|
|
||||||
|
|
||||||
public override string ExecutableName => "RobloxPlayerBeta.exe";
|
|
||||||
|
|
||||||
public override AppState State => App.State.Prop.Player;
|
|
||||||
|
|
||||||
public override IReadOnlyDictionary<string, string> PackageDirectoryMap { get; set; } = new Dictionary<string, string>()
|
|
||||||
{
|
|
||||||
{ "RobloxApp.zip", @"" }
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,36 +0,0 @@
|
|||||||
namespace Bloxstrap.AppData
|
|
||||||
{
|
|
||||||
public class RobloxStudioData : CommonAppData, IAppData
|
|
||||||
{
|
|
||||||
public string ProductName => "Roblox Studio";
|
|
||||||
|
|
||||||
public string BinaryType => "WindowsStudio64";
|
|
||||||
|
|
||||||
public string RegistryName => "RobloxStudio";
|
|
||||||
|
|
||||||
public override string ExecutableName => "RobloxStudioBeta.exe";
|
|
||||||
|
|
||||||
public override AppState State => App.State.Prop.Studio;
|
|
||||||
|
|
||||||
public override IReadOnlyDictionary<string, string> PackageDirectoryMap { get; set; } = new Dictionary<string, string>()
|
|
||||||
{
|
|
||||||
{ "RobloxStudio.zip", @"" },
|
|
||||||
{ "LibrariesQt5.zip", @"" },
|
|
||||||
|
|
||||||
{ "content-studio_svg_textures.zip", @"content\studio_svg_textures\"},
|
|
||||||
{ "content-qt_translations.zip", @"content\qt_translations\" },
|
|
||||||
{ "content-api-docs.zip", @"content\api_docs\" },
|
|
||||||
|
|
||||||
{ "extracontent-scripts.zip", @"ExtraContent\scripts\" },
|
|
||||||
|
|
||||||
{ "BuiltInPlugins.zip", @"BuiltInPlugins\" },
|
|
||||||
{ "BuiltInStandalonePlugins.zip", @"BuiltInStandalonePlugins\" },
|
|
||||||
|
|
||||||
{ "ApplicationConfig.zip", @"ApplicationConfig\" },
|
|
||||||
{ "Plugins.zip", @"Plugins\" },
|
|
||||||
{ "Qml.zip", @"Qml\" },
|
|
||||||
{ "StudioFonts.zip", @"StudioFonts\" },
|
|
||||||
{ "RibbonConfig.zip", @"RibbonConfig\" }
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,10 +0,0 @@
|
|||||||
using System.Windows;
|
|
||||||
|
|
||||||
[assembly: ThemeInfo(
|
|
||||||
ResourceDictionaryLocation.None, //where theme specific resource dictionaries are located
|
|
||||||
//(used if a resource is not found in the page,
|
|
||||||
// or application resource dictionaries)
|
|
||||||
ResourceDictionaryLocation.SourceAssembly //where the generic resource dictionary is located
|
|
||||||
//(used if a resource is not found in the page,
|
|
||||||
// app, or any theme specific resource dictionaries)
|
|
||||||
)]
|
|
@ -1,114 +0,0 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
|
||||||
|
|
||||||
<PropertyGroup>
|
|
||||||
<OutputType>WinExe</OutputType>
|
|
||||||
<TargetFramework>net6.0-windows</TargetFramework>
|
|
||||||
<Nullable>enable</Nullable>
|
|
||||||
<UseWPF>true</UseWPF>
|
|
||||||
<UseWindowsForms>True</UseWindowsForms>
|
|
||||||
<ApplicationIcon>Bloxstrap.ico</ApplicationIcon>
|
|
||||||
<Version>2.9.0</Version>
|
|
||||||
<FileVersion>2.9.0</FileVersion>
|
|
||||||
<ApplicationManifest>app.manifest</ApplicationManifest>
|
|
||||||
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
|
||||||
<IncludeSourceRevisionInInformationalVersion>false</IncludeSourceRevisionInInformationalVersion>
|
|
||||||
</PropertyGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<Resource Include="Bloxstrap.ico" />
|
|
||||||
<Resource Include="Resources\Fonts\NotoSansThai-VariableFont_wdth,wght.ttf" />
|
|
||||||
<Resource Include="Resources\Fonts\Rubik-VariableFont_wght.ttf" />
|
|
||||||
<Resource Include="Resources\BootstrapperStyles\ByfronDialog\ByfronLogoDark.jpg" />
|
|
||||||
<Resource Include="Resources\BootstrapperStyles\ByfronDialog\ByfronLogoLight.jpg" />
|
|
||||||
<Resource Include="Resources\BootstrapperStyles\ByfronDialog\Matt.png" />
|
|
||||||
<Resource Include="Resources\MessageBox\Error.png" />
|
|
||||||
<Resource Include="Resources\MessageBox\Information.png" />
|
|
||||||
<Resource Include="Resources\MessageBox\Question.png" />
|
|
||||||
<Resource Include="Resources\MessageBox\Warning.png" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<EmbeddedResource Include="Resources\Icon2008.ico" />
|
|
||||||
<EmbeddedResource Include="Resources\Icon2011.ico" />
|
|
||||||
<EmbeddedResource Include="Resources\Icon2017.ico" />
|
|
||||||
<EmbeddedResource Include="Resources\Icon2019.ico" />
|
|
||||||
<EmbeddedResource Include="Resources\Icon2022.ico" />
|
|
||||||
<EmbeddedResource Include="Resources\IconBloxstrap.ico" />
|
|
||||||
<EmbeddedResource Include="Resources\IconEarly2015.ico" />
|
|
||||||
<EmbeddedResource Include="Resources\IconLate2015.ico" />
|
|
||||||
<EmbeddedResource Include="Resources\Mods\Cursor\From2006\ArrowCursor.png" />
|
|
||||||
<EmbeddedResource Include="Resources\Mods\Cursor\From2006\ArrowFarCursor.png" />
|
|
||||||
<EmbeddedResource Include="Resources\Mods\Cursor\From2013\ArrowCursor.png" />
|
|
||||||
<EmbeddedResource Include="Resources\Mods\Cursor\From2013\ArrowFarCursor.png" />
|
|
||||||
<EmbeddedResource Include="Resources\Mods\Sounds\OldDeath.ogg" />
|
|
||||||
<EmbeddedResource Include="Resources\Mods\Sounds\OldGetUp.mp3" />
|
|
||||||
<EmbeddedResource Include="Resources\Mods\Sounds\OldJump.mp3" />
|
|
||||||
<EmbeddedResource Include="Resources\Mods\Sounds\OldWalk.mp3" />
|
|
||||||
<EmbeddedResource Include="Resources\Mods\Sounds\Empty.mp3" />
|
|
||||||
<EmbeddedResource Include="Resources\Mods\OldAvatarBackground.rbxl" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.4.0" />
|
|
||||||
<PackageReference Include="DiscordRichPresence" Version="1.2.1.24" />
|
|
||||||
<PackageReference Include="Markdig" Version="0.40.0" />
|
|
||||||
<PackageReference Include="Microsoft.Windows.CsWin32" Version="0.3.183">
|
|
||||||
<!--<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>-->
|
|
||||||
<PrivateAssets>all</PrivateAssets>
|
|
||||||
</PackageReference>
|
|
||||||
<PackageReference Include="securifybv.ShellLink" Version="0.1.0" />
|
|
||||||
<PackageReference Include="SharpZipLib" Version="1.4.2" />
|
|
||||||
<PackageReference Include="System.Resources.ResourceManager" Version="4.3.0" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<ProjectReference Include="..\wpfui\src\Wpf.Ui\Wpf.Ui.csproj" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<AssemblyAttribute Include="Bloxstrap.Models.Attributes.BuildMetadataAttribute">
|
|
||||||
<_Parameter1>$([System.DateTime]::UtcNow.ToString("s"))Z</_Parameter1>
|
|
||||||
<_Parameter2>$(COMPUTERNAME)\$(USERNAME)</_Parameter2>
|
|
||||||
<_Parameter3>$(CommitHash)</_Parameter3>
|
|
||||||
<_Parameter4>$(CommitRef)</_Parameter4>
|
|
||||||
</AssemblyAttribute>
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<AdditionalFiles Include="NativeMethods.txt" />
|
|
||||||
|
|
||||||
<!-- Provide the path to the winmds used as input into the analyzer. -->
|
|
||||||
<CompilerVisibleProperty Include="CsWin32InputMetadataPaths" />
|
|
||||||
<CompilerVisibleProperty Include="CsWin32InputDocPaths" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
<Target Name="AssembleCsWin32InputPaths" BeforeTargets="GenerateMSBuildEditorConfigFileCore">
|
|
||||||
<!-- Roslyn only allows source generators to see msbuild properties, to lift msbuild items into semicolon-delimited properties. -->
|
|
||||||
<PropertyGroup>
|
|
||||||
<CsWin32InputMetadataPaths>@(ProjectionMetadataWinmd->'%(FullPath)','|')</CsWin32InputMetadataPaths>
|
|
||||||
<CsWin32InputDocPaths>@(ProjectionDocs->'%(FullPath)','|')</CsWin32InputDocPaths>
|
|
||||||
</PropertyGroup>
|
|
||||||
</Target>
|
|
||||||
|
|
||||||
<Target Name="FixMds" BeforeTargets="CoreCompile" Condition="'@(ProjectionMetadataWinmd)'==''">
|
|
||||||
<ItemGroup>
|
|
||||||
<ProjectionMetadataWinmd Include="$(UserProfile)\.nuget\packages\microsoft.windows.sdk.win32metadata\55.0.45-preview\Windows.Win32.winmd" />
|
|
||||||
</ItemGroup>
|
|
||||||
</Target>
|
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<Compile Update="Resources\Strings.Designer.cs">
|
|
||||||
<DesignTime>True</DesignTime>
|
|
||||||
<AutoGen>True</AutoGen>
|
|
||||||
<DependentUpon>Strings.resx</DependentUpon>
|
|
||||||
</Compile>
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<EmbeddedResource Update="Resources\Strings.resx">
|
|
||||||
<Generator>PublicResXFileCodeGenerator</Generator>
|
|
||||||
<LastGenOutput>Strings.Designer.cs</LastGenOutput>
|
|
||||||
</EmbeddedResource>
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
</Project>
|
|
Binary file not shown.
Before Width: | Height: | Size: 130 KiB |
File diff suppressed because it is too large
Load Diff
@ -1,24 +0,0 @@
|
|||||||
namespace Bloxstrap.Enums
|
|
||||||
{
|
|
||||||
public enum BootstrapperIcon
|
|
||||||
{
|
|
||||||
[EnumName(StaticName = "Bloxstrap")]
|
|
||||||
IconBloxstrap,
|
|
||||||
[EnumName(StaticName = "2008")]
|
|
||||||
Icon2008,
|
|
||||||
[EnumName(StaticName = "2011")]
|
|
||||||
Icon2011,
|
|
||||||
IconEarly2015,
|
|
||||||
IconLate2015,
|
|
||||||
[EnumName(StaticName = "2017")]
|
|
||||||
Icon2017,
|
|
||||||
[EnumName(StaticName = "2019")]
|
|
||||||
Icon2019,
|
|
||||||
[EnumName(StaticName = "2022")]
|
|
||||||
Icon2022,
|
|
||||||
[EnumName(FromTranslation = "Common.Custom")]
|
|
||||||
IconCustom,
|
|
||||||
[EnumName(FromTranslation = "Enums.BootstrapperStyle.ClassicFluentDialog")]
|
|
||||||
IconBloxstrapClassic
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,15 +0,0 @@
|
|||||||
namespace Bloxstrap.Enums
|
|
||||||
{
|
|
||||||
public enum BootstrapperStyle
|
|
||||||
{
|
|
||||||
VistaDialog,
|
|
||||||
LegacyDialog2008,
|
|
||||||
LegacyDialog2011,
|
|
||||||
ProgressDialog,
|
|
||||||
ClassicFluentDialog,
|
|
||||||
ByfronDialog,
|
|
||||||
[EnumName(StaticName = "Bloxstrap")]
|
|
||||||
FluentDialog,
|
|
||||||
FluentAeroDialog
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,15 +0,0 @@
|
|||||||
namespace Bloxstrap.Enums
|
|
||||||
{
|
|
||||||
public enum CursorType
|
|
||||||
{
|
|
||||||
[EnumSort(Order = 1)]
|
|
||||||
[EnumName(FromTranslation = "Common.Default")]
|
|
||||||
Default,
|
|
||||||
|
|
||||||
[EnumSort(Order = 3)]
|
|
||||||
From2006,
|
|
||||||
|
|
||||||
[EnumSort(Order = 2)]
|
|
||||||
From2013
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,11 +0,0 @@
|
|||||||
namespace Bloxstrap.Enums
|
|
||||||
{
|
|
||||||
public enum EmojiType
|
|
||||||
{
|
|
||||||
Default,
|
|
||||||
Catmoji,
|
|
||||||
Windows11,
|
|
||||||
Windows10,
|
|
||||||
Windows8
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,20 +0,0 @@
|
|||||||
namespace Bloxstrap.Enums
|
|
||||||
{
|
|
||||||
// https://learn.microsoft.com/en-us/windows/win32/msi/error-codes
|
|
||||||
// https://i-logic.com/serial/errorcodes.htm
|
|
||||||
// https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-erref/705fb797-2175-4a90-b5a3-3918024b10b8
|
|
||||||
// just the ones that we're interested in
|
|
||||||
|
|
||||||
public enum ErrorCode
|
|
||||||
{
|
|
||||||
ERROR_SUCCESS = 0,
|
|
||||||
ERROR_INVALID_FUNCTION = 1,
|
|
||||||
ERROR_FILE_NOT_FOUND = 2,
|
|
||||||
|
|
||||||
ERROR_CANCELLED = 1223,
|
|
||||||
ERROR_INSTALL_USEREXIT = 1602,
|
|
||||||
ERROR_INSTALL_FAILURE = 1603,
|
|
||||||
|
|
||||||
CO_E_APPNOTFOUND = -2147221003
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,12 +0,0 @@
|
|||||||
namespace Bloxstrap.Enums.FlagPresets
|
|
||||||
{
|
|
||||||
public enum InGameMenuVersion
|
|
||||||
{
|
|
||||||
[EnumName(FromTranslation = "Common.Default")]
|
|
||||||
Default,
|
|
||||||
V1,
|
|
||||||
V2,
|
|
||||||
V4,
|
|
||||||
V4Chrome
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,10 +0,0 @@
|
|||||||
namespace Bloxstrap.Enums.FlagPresets
|
|
||||||
{
|
|
||||||
public enum LightingMode
|
|
||||||
{
|
|
||||||
Default,
|
|
||||||
Voxel,
|
|
||||||
ShadowMap,
|
|
||||||
Future
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,14 +0,0 @@
|
|||||||
namespace Bloxstrap.Enums.FlagPresets
|
|
||||||
{
|
|
||||||
public enum MSAAMode
|
|
||||||
{
|
|
||||||
[EnumName(FromTranslation = "Common.Automatic")]
|
|
||||||
Default,
|
|
||||||
[EnumName(StaticName = "1x")]
|
|
||||||
x1,
|
|
||||||
[EnumName(StaticName = "2x")]
|
|
||||||
x2,
|
|
||||||
[EnumName(StaticName = "4x")]
|
|
||||||
x4
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,10 +0,0 @@
|
|||||||
namespace Bloxstrap.Enums.FlagPresets
|
|
||||||
{
|
|
||||||
public enum RenderingMode
|
|
||||||
{
|
|
||||||
[EnumName(FromTranslation = "Common.Automatic")]
|
|
||||||
Default,
|
|
||||||
D3D11,
|
|
||||||
D3D10,
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,12 +0,0 @@
|
|||||||
namespace Bloxstrap.Enums.FlagPresets
|
|
||||||
{
|
|
||||||
public enum TextureQuality
|
|
||||||
{
|
|
||||||
[EnumName(FromTranslation = "Common.Automatic")]
|
|
||||||
Default,
|
|
||||||
Level0,
|
|
||||||
Level1,
|
|
||||||
Level2,
|
|
||||||
Level3
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,9 +0,0 @@
|
|||||||
namespace Bloxstrap.Enums
|
|
||||||
{
|
|
||||||
public enum GenericTriState
|
|
||||||
{
|
|
||||||
Successful,
|
|
||||||
Failed,
|
|
||||||
Unknown
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,10 +0,0 @@
|
|||||||
namespace Bloxstrap.Enums
|
|
||||||
{
|
|
||||||
public enum LaunchMode
|
|
||||||
{
|
|
||||||
None,
|
|
||||||
Player,
|
|
||||||
Studio,
|
|
||||||
StudioAuth
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,10 +0,0 @@
|
|||||||
namespace Bloxstrap.Enums
|
|
||||||
{
|
|
||||||
public enum NextAction
|
|
||||||
{
|
|
||||||
Terminate,
|
|
||||||
LaunchSettings,
|
|
||||||
LaunchRoblox,
|
|
||||||
LaunchRobloxStudio
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,9 +0,0 @@
|
|||||||
namespace Bloxstrap.Enums
|
|
||||||
{
|
|
||||||
public enum ServerType
|
|
||||||
{
|
|
||||||
Public,
|
|
||||||
Private,
|
|
||||||
Reserved
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,10 +0,0 @@
|
|||||||
namespace Bloxstrap.Enums
|
|
||||||
{
|
|
||||||
public enum Theme
|
|
||||||
{
|
|
||||||
[EnumName(FromTranslation = "Common.SystemDefault")]
|
|
||||||
Default,
|
|
||||||
Light,
|
|
||||||
Dark
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,9 +0,0 @@
|
|||||||
namespace Bloxstrap.Enums
|
|
||||||
{
|
|
||||||
enum VersionComparison
|
|
||||||
{
|
|
||||||
LessThan = -1,
|
|
||||||
Equal = 0,
|
|
||||||
GreaterThan = 1
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,16 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Text;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
namespace Bloxstrap.Exceptions
|
|
||||||
{
|
|
||||||
internal class AssertionException : Exception
|
|
||||||
{
|
|
||||||
public AssertionException(string message)
|
|
||||||
: base($"{message}\n\nThis is very likely just an off-chance error. Please report this first, and then start {App.ProjectName} again.")
|
|
||||||
{
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,15 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Text;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
namespace Bloxstrap.Exceptions
|
|
||||||
{
|
|
||||||
internal class ChecksumFailedException : Exception
|
|
||||||
{
|
|
||||||
public ChecksumFailedException(string message) : base(message)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,10 +0,0 @@
|
|||||||
namespace Bloxstrap.Exceptions
|
|
||||||
{
|
|
||||||
public class InvalidChannelException : Exception
|
|
||||||
{
|
|
||||||
public HttpStatusCode? StatusCode;
|
|
||||||
|
|
||||||
public InvalidChannelException(HttpStatusCode? statusCode) : base()
|
|
||||||
=> StatusCode = statusCode;
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,13 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Text;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
namespace Bloxstrap.Exceptions
|
|
||||||
{
|
|
||||||
internal class InvalidHTTPResponseException : Exception
|
|
||||||
{
|
|
||||||
public InvalidHTTPResponseException(string message) : base(message) { }
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,70 +0,0 @@
|
|||||||
using System.Drawing;
|
|
||||||
|
|
||||||
namespace Bloxstrap.Extensions
|
|
||||||
{
|
|
||||||
static class BootstrapperIconEx
|
|
||||||
{
|
|
||||||
public static IReadOnlyCollection<BootstrapperIcon> Selections => new BootstrapperIcon[]
|
|
||||||
{
|
|
||||||
BootstrapperIcon.IconBloxstrap,
|
|
||||||
BootstrapperIcon.Icon2022,
|
|
||||||
BootstrapperIcon.Icon2019,
|
|
||||||
BootstrapperIcon.Icon2017,
|
|
||||||
BootstrapperIcon.IconLate2015,
|
|
||||||
BootstrapperIcon.IconEarly2015,
|
|
||||||
BootstrapperIcon.Icon2011,
|
|
||||||
BootstrapperIcon.Icon2008,
|
|
||||||
BootstrapperIcon.IconBloxstrapClassic,
|
|
||||||
BootstrapperIcon.IconCustom
|
|
||||||
};
|
|
||||||
|
|
||||||
// small note on handling icon sizes
|
|
||||||
// i'm using multisize icon packs here with sizes 16, 24, 32, 48, 64 and 128
|
|
||||||
// use this for generating multisize packs: https://www.aconvert.com/icon/
|
|
||||||
|
|
||||||
public static Icon GetIcon(this BootstrapperIcon icon)
|
|
||||||
{
|
|
||||||
const string LOG_IDENT = "BootstrapperIconEx::GetIcon";
|
|
||||||
|
|
||||||
// load the custom icon file
|
|
||||||
if (icon == BootstrapperIcon.IconCustom)
|
|
||||||
{
|
|
||||||
Icon? customIcon = null;
|
|
||||||
string location = App.Settings.Prop.BootstrapperIconCustomLocation;
|
|
||||||
|
|
||||||
if (String.IsNullOrEmpty(location))
|
|
||||||
{
|
|
||||||
App.Logger.WriteLine(LOG_IDENT, "Warning: custom icon is not set.");
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
customIcon = new Icon(location);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
App.Logger.WriteLine(LOG_IDENT, $"Failed to load custom icon!");
|
|
||||||
App.Logger.WriteException(LOG_IDENT, ex);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return customIcon ?? Properties.Resources.IconBloxstrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
return icon switch
|
|
||||||
{
|
|
||||||
BootstrapperIcon.IconBloxstrap => Properties.Resources.IconBloxstrap,
|
|
||||||
BootstrapperIcon.Icon2008 => Properties.Resources.Icon2008,
|
|
||||||
BootstrapperIcon.Icon2011 => Properties.Resources.Icon2011,
|
|
||||||
BootstrapperIcon.IconEarly2015 => Properties.Resources.IconEarly2015,
|
|
||||||
BootstrapperIcon.IconLate2015 => Properties.Resources.IconLate2015,
|
|
||||||
BootstrapperIcon.Icon2017 => Properties.Resources.Icon2017,
|
|
||||||
BootstrapperIcon.Icon2019 => Properties.Resources.Icon2019,
|
|
||||||
BootstrapperIcon.Icon2022 => Properties.Resources.Icon2022,
|
|
||||||
BootstrapperIcon.IconBloxstrapClassic => Properties.Resources.IconBloxstrapClassic,
|
|
||||||
_ => Properties.Resources.IconBloxstrap
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,19 +0,0 @@
|
|||||||
namespace Bloxstrap.Extensions
|
|
||||||
{
|
|
||||||
static class BootstrapperStyleEx
|
|
||||||
{
|
|
||||||
public static IBootstrapperDialog GetNew(this BootstrapperStyle bootstrapperStyle) => Frontend.GetBootstrapperDialog(bootstrapperStyle);
|
|
||||||
|
|
||||||
public static IReadOnlyCollection<BootstrapperStyle> Selections => new BootstrapperStyle[]
|
|
||||||
{
|
|
||||||
BootstrapperStyle.FluentDialog,
|
|
||||||
BootstrapperStyle.FluentAeroDialog,
|
|
||||||
BootstrapperStyle.ClassicFluentDialog,
|
|
||||||
BootstrapperStyle.ByfronDialog,
|
|
||||||
BootstrapperStyle.ProgressDialog,
|
|
||||||
BootstrapperStyle.LegacyDialog2011,
|
|
||||||
BootstrapperStyle.LegacyDialog2008,
|
|
||||||
BootstrapperStyle.VistaDialog
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,10 +0,0 @@
|
|||||||
namespace Bloxstrap.Extensions
|
|
||||||
{
|
|
||||||
static class DateTimeEx
|
|
||||||
{
|
|
||||||
public static string ToFriendlyString(this DateTime dateTime)
|
|
||||||
{
|
|
||||||
return dateTime.ToString("dddd, d MMMM yyyy 'at' h:mm:ss tt", CultureInfo.InvariantCulture);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,31 +0,0 @@
|
|||||||
namespace Bloxstrap.Extensions
|
|
||||||
{
|
|
||||||
static class EmojiTypeEx
|
|
||||||
{
|
|
||||||
public static IReadOnlyDictionary<EmojiType, string> Filenames => new Dictionary<EmojiType, string>
|
|
||||||
{
|
|
||||||
{ EmojiType.Catmoji, "Catmoji.ttf" },
|
|
||||||
{ EmojiType.Windows11, "Win1122H2SegoeUIEmoji.ttf" },
|
|
||||||
{ EmojiType.Windows10, "Win10April2018SegoeUIEmoji.ttf" },
|
|
||||||
{ EmojiType.Windows8, "Win8.1SegoeUIEmoji.ttf" },
|
|
||||||
};
|
|
||||||
|
|
||||||
public static IReadOnlyDictionary<EmojiType, string> Hashes => new Dictionary<EmojiType, string>
|
|
||||||
{
|
|
||||||
{ EmojiType.Catmoji, "98138f398a8cde897074dd2b8d53eca0" },
|
|
||||||
{ EmojiType.Windows11, "d50758427673578ddf6c9edcdbf367f5" },
|
|
||||||
{ EmojiType.Windows10, "d8a7eecbebf9dfdf622db8ccda63aff5" },
|
|
||||||
{ EmojiType.Windows8, "2b01c6caabbe95afc92aa63b9bf100f3" },
|
|
||||||
};
|
|
||||||
|
|
||||||
public static string GetHash(this EmojiType emojiType) => Hashes[emojiType];
|
|
||||||
|
|
||||||
public static string GetUrl(this EmojiType emojiType)
|
|
||||||
{
|
|
||||||
if (emojiType == EmojiType.Default)
|
|
||||||
return "";
|
|
||||||
|
|
||||||
return $"https://github.com/bloxstraplabs/rbxcustom-fontemojis/releases/download/my-phone-is-78-percent/{Filenames[emojiType]}";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,35 +0,0 @@
|
|||||||
using System.Drawing;
|
|
||||||
using System.Windows.Media.Imaging;
|
|
||||||
using System.Windows.Media;
|
|
||||||
|
|
||||||
namespace Bloxstrap.Extensions
|
|
||||||
{
|
|
||||||
public static class IconEx
|
|
||||||
{
|
|
||||||
public static Icon GetSized(this Icon icon, int width, int height) => new(icon, new Size(width, height));
|
|
||||||
|
|
||||||
public static ImageSource GetImageSource(this Icon icon, bool handleException = true)
|
|
||||||
{
|
|
||||||
using MemoryStream stream = new();
|
|
||||||
icon.Save(stream);
|
|
||||||
|
|
||||||
if (handleException)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
return BitmapFrame.Create(stream, BitmapCreateOptions.None, BitmapCacheOption.OnLoad);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
App.Logger.WriteException("IconEx::GetImageSource", ex);
|
|
||||||
Frontend.ShowMessageBox(String.Format(Strings.Dialog_IconLoadFailed, ex.Message));
|
|
||||||
return BootstrapperIcon.IconBloxstrap.GetIcon().GetImageSource(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
return BitmapFrame.Create(stream, BitmapCreateOptions.None, BitmapCacheOption.OnLoad);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,35 +0,0 @@
|
|||||||
using Microsoft.Win32;
|
|
||||||
|
|
||||||
namespace Bloxstrap.Extensions
|
|
||||||
{
|
|
||||||
public static class RegistryKeyEx
|
|
||||||
{
|
|
||||||
public static void SetValueSafe(this RegistryKey registryKey, string? name, object value)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
App.Logger.WriteLine("RegistryKeyEx::SetValueSafe", $"Writing '{value}' to {registryKey}\\{name}");
|
|
||||||
registryKey.SetValue(name, value);
|
|
||||||
}
|
|
||||||
catch (UnauthorizedAccessException)
|
|
||||||
{
|
|
||||||
Frontend.ShowMessageBox(Strings.Dialog_RegistryWriteError, System.Windows.MessageBoxImage.Error);
|
|
||||||
App.Terminate(ErrorCode.ERROR_INSTALL_FAILURE);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void DeleteValueSafe(this RegistryKey registryKey, string name)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
App.Logger.WriteLine("RegistryKeyEx::DeleteValueSafe", $"Deleting {registryKey}\\{name}");
|
|
||||||
registryKey.DeleteValue(name);
|
|
||||||
}
|
|
||||||
catch (UnauthorizedAccessException)
|
|
||||||
{
|
|
||||||
Frontend.ShowMessageBox(Strings.Dialog_RegistryWriteError, System.Windows.MessageBoxImage.Error);
|
|
||||||
App.Terminate(ErrorCode.ERROR_INSTALL_FAILURE);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,24 +0,0 @@
|
|||||||
using System.Resources;
|
|
||||||
|
|
||||||
namespace Bloxstrap.Extensions
|
|
||||||
{
|
|
||||||
static class ResourceManagerEx
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Returns the value of the specified string resource. <br/>
|
|
||||||
/// If the resource is not found, the resource name will be returned.
|
|
||||||
/// </summary>
|
|
||||||
public static string GetStringSafe(this ResourceManager manager, string name) => manager.GetStringSafe(name, null);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Returns the value of the string resource localized for the specified culture. <br/>
|
|
||||||
/// If the resource is not found, the resource name will be returned.
|
|
||||||
/// </summary>
|
|
||||||
public static string GetStringSafe(this ResourceManager manager, string name, CultureInfo? culture)
|
|
||||||
{
|
|
||||||
string? resourceValue = manager.GetString(name, culture);
|
|
||||||
|
|
||||||
return resourceValue ?? name;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,13 +0,0 @@
|
|||||||
namespace Bloxstrap.Extensions
|
|
||||||
{
|
|
||||||
static class ServerTypeEx
|
|
||||||
{
|
|
||||||
public static string ToTranslatedString(this ServerType value) => value switch
|
|
||||||
{
|
|
||||||
ServerType.Public => Strings.Enums_ServerType_Public,
|
|
||||||
ServerType.Private => Strings.Enums_ServerType_Private,
|
|
||||||
ServerType.Reserved => Strings.Enums_ServerType_Reserved,
|
|
||||||
_ => "?"
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,20 +0,0 @@
|
|||||||
using Microsoft.Win32;
|
|
||||||
|
|
||||||
namespace Bloxstrap.Extensions
|
|
||||||
{
|
|
||||||
public static class ThemeEx
|
|
||||||
{
|
|
||||||
public static Theme GetFinal(this Theme dialogTheme)
|
|
||||||
{
|
|
||||||
if (dialogTheme != Theme.Default)
|
|
||||||
return dialogTheme;
|
|
||||||
|
|
||||||
using var key = Registry.CurrentUser.OpenSubKey("SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize");
|
|
||||||
|
|
||||||
if (key?.GetValue("AppsUseLightTheme") is int value && value == 0)
|
|
||||||
return Theme.Dark;
|
|
||||||
|
|
||||||
return Theme.Light;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,263 +0,0 @@
|
|||||||
using Bloxstrap.Enums.FlagPresets;
|
|
||||||
|
|
||||||
namespace Bloxstrap
|
|
||||||
{
|
|
||||||
public class FastFlagManager : JsonManager<Dictionary<string, object>>
|
|
||||||
{
|
|
||||||
public override string ClassName => nameof(FastFlagManager);
|
|
||||||
|
|
||||||
public override string LOG_IDENT_CLASS => ClassName;
|
|
||||||
|
|
||||||
public override string FileLocation => Path.Combine(Paths.Modifications, "ClientSettings\\ClientAppSettings.json");
|
|
||||||
|
|
||||||
public bool Changed => !OriginalProp.SequenceEqual(Prop);
|
|
||||||
|
|
||||||
public static IReadOnlyDictionary<string, string> PresetFlags = new Dictionary<string, string>
|
|
||||||
{
|
|
||||||
{ "Network.Log", "FLogNetwork" },
|
|
||||||
|
|
||||||
{ "Rendering.Framerate", "DFIntTaskSchedulerTargetFps" },
|
|
||||||
{ "Rendering.ManualFullscreen", "FFlagHandleAltEnterFullscreenManually" },
|
|
||||||
{ "Rendering.DisableScaling", "DFFlagDisableDPIScale" },
|
|
||||||
{ "Rendering.MSAA", "FIntDebugForceMSAASamples" },
|
|
||||||
{ "Rendering.DisablePostFX", "FFlagDisablePostFx" },
|
|
||||||
{ "Rendering.ShadowIntensity", "FIntRenderShadowIntensity" },
|
|
||||||
|
|
||||||
{ "Rendering.Mode.D3D11", "FFlagDebugGraphicsPreferD3D11" },
|
|
||||||
{ "Rendering.Mode.D3D10", "FFlagDebugGraphicsPreferD3D11FL10" },
|
|
||||||
|
|
||||||
{ "Rendering.Lighting.Voxel", "DFFlagDebugRenderForceTechnologyVoxel" },
|
|
||||||
{ "Rendering.Lighting.ShadowMap", "FFlagDebugForceFutureIsBrightPhase2" },
|
|
||||||
{ "Rendering.Lighting.Future", "FFlagDebugForceFutureIsBrightPhase3" },
|
|
||||||
|
|
||||||
{ "Rendering.TextureQuality.OverrideEnabled", "DFFlagTextureQualityOverrideEnabled" },
|
|
||||||
{ "Rendering.TextureQuality.Level", "DFIntTextureQualityOverride" },
|
|
||||||
{ "Rendering.TerrainTextureQuality", "FIntTerrainArraySliceSize" },
|
|
||||||
|
|
||||||
{ "UI.Hide", "DFIntCanHideGuiGroupId" },
|
|
||||||
{ "UI.FontSize", "FIntFontSizePadding" },
|
|
||||||
|
|
||||||
{ "UI.FullscreenTitlebarDelay", "FIntFullscreenTitleBarTriggerDelayMillis" },
|
|
||||||
|
|
||||||
//{ "UI.Menu.Style.V2Rollout", "FIntNewInGameMenuPercentRollout3" },
|
|
||||||
//{ "UI.Menu.Style.EnableV4.1", "FFlagEnableInGameMenuControls" },
|
|
||||||
//{ "UI.Menu.Style.EnableV4.2", "FFlagEnableInGameMenuModernization" },
|
|
||||||
//{ "UI.Menu.Style.EnableV4Chrome", "FFlagEnableInGameMenuChrome" },
|
|
||||||
//{ "UI.Menu.Style.ReportButtonCutOff", "FFlagFixReportButtonCutOff" },
|
|
||||||
|
|
||||||
|
|
||||||
//{ "UI.Menu.Style.ABTest.1", "FFlagEnableMenuControlsABTest" },
|
|
||||||
//{ "UI.Menu.Style.ABTest.2", "FFlagEnableV3MenuABTest3" },
|
|
||||||
//{ "UI.Menu.Style.ABTest.3", "FFlagEnableInGameMenuChromeABTest3" },
|
|
||||||
//{ "UI.Menu.Style.ABTest.4", "FFlagEnableInGameMenuChromeABTest4" }
|
|
||||||
};
|
|
||||||
|
|
||||||
public static IReadOnlyDictionary<RenderingMode, string> RenderingModes => new Dictionary<RenderingMode, string>
|
|
||||||
{
|
|
||||||
{ RenderingMode.Default, "None" },
|
|
||||||
{ RenderingMode.D3D11, "D3D11" },
|
|
||||||
{ RenderingMode.D3D10, "D3D10" },
|
|
||||||
};
|
|
||||||
|
|
||||||
public static IReadOnlyDictionary<LightingMode, string> LightingModes => new Dictionary<LightingMode, string>
|
|
||||||
{
|
|
||||||
{ LightingMode.Default, "None" },
|
|
||||||
{ LightingMode.Voxel, "Voxel" },
|
|
||||||
{ LightingMode.ShadowMap, "ShadowMap" },
|
|
||||||
{ LightingMode.Future, "Future" }
|
|
||||||
};
|
|
||||||
|
|
||||||
public static IReadOnlyDictionary<MSAAMode, string?> MSAAModes => new Dictionary<MSAAMode, string?>
|
|
||||||
{
|
|
||||||
{ MSAAMode.Default, null },
|
|
||||||
{ MSAAMode.x1, "1" },
|
|
||||||
{ MSAAMode.x2, "2" },
|
|
||||||
{ MSAAMode.x4, "4" }
|
|
||||||
};
|
|
||||||
|
|
||||||
public static IReadOnlyDictionary<TextureQuality, string?> TextureQualityLevels => new Dictionary<TextureQuality, string?>
|
|
||||||
{
|
|
||||||
{ TextureQuality.Default, null },
|
|
||||||
{ TextureQuality.Level0, "0" },
|
|
||||||
{ TextureQuality.Level1, "1" },
|
|
||||||
{ TextureQuality.Level2, "2" },
|
|
||||||
{ TextureQuality.Level3, "3" },
|
|
||||||
};
|
|
||||||
|
|
||||||
// this is one hell of a dictionary definition lmao
|
|
||||||
// since these all set the same flags, wouldn't making this use bitwise operators be better?
|
|
||||||
//public static IReadOnlyDictionary<InGameMenuVersion, Dictionary<string, string?>> IGMenuVersions => new Dictionary<InGameMenuVersion, Dictionary<string, string?>>
|
|
||||||
//{
|
|
||||||
// {
|
|
||||||
// InGameMenuVersion.Default,
|
|
||||||
// new Dictionary<string, string?>
|
|
||||||
// {
|
|
||||||
// { "V2Rollout", null },
|
|
||||||
// { "EnableV4", null },
|
|
||||||
// { "EnableV4Chrome", null },
|
|
||||||
// { "ABTest", null },
|
|
||||||
// { "ReportButtonCutOff", null }
|
|
||||||
// }
|
|
||||||
// },
|
|
||||||
|
|
||||||
// {
|
|
||||||
// InGameMenuVersion.V1,
|
|
||||||
// new Dictionary<string, string?>
|
|
||||||
// {
|
|
||||||
// { "V2Rollout", "0" },
|
|
||||||
// { "EnableV4", "False" },
|
|
||||||
// { "EnableV4Chrome", "False" },
|
|
||||||
// { "ABTest", "False" },
|
|
||||||
// { "ReportButtonCutOff", "False" }
|
|
||||||
// }
|
|
||||||
// },
|
|
||||||
|
|
||||||
// {
|
|
||||||
// InGameMenuVersion.V2,
|
|
||||||
// new Dictionary<string, string?>
|
|
||||||
// {
|
|
||||||
// { "V2Rollout", "100" },
|
|
||||||
// { "EnableV4", "False" },
|
|
||||||
// { "EnableV4Chrome", "False" },
|
|
||||||
// { "ABTest", "False" },
|
|
||||||
// { "ReportButtonCutOff", null }
|
|
||||||
// }
|
|
||||||
// },
|
|
||||||
|
|
||||||
// {
|
|
||||||
// InGameMenuVersion.V4,
|
|
||||||
// new Dictionary<string, string?>
|
|
||||||
// {
|
|
||||||
// { "V2Rollout", "0" },
|
|
||||||
// { "EnableV4", "True" },
|
|
||||||
// { "EnableV4Chrome", "False" },
|
|
||||||
// { "ABTest", "False" },
|
|
||||||
// { "ReportButtonCutOff", null }
|
|
||||||
// }
|
|
||||||
// },
|
|
||||||
|
|
||||||
// {
|
|
||||||
// InGameMenuVersion.V4Chrome,
|
|
||||||
// new Dictionary<string, string?>
|
|
||||||
// {
|
|
||||||
// { "V2Rollout", "0" },
|
|
||||||
// { "EnableV4", "True" },
|
|
||||||
// { "EnableV4Chrome", "True" },
|
|
||||||
// { "ABTest", "False" },
|
|
||||||
// { "ReportButtonCutOff", null }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
//};
|
|
||||||
|
|
||||||
// all fflags are stored as strings
|
|
||||||
// to delete a flag, set the value as null
|
|
||||||
public void SetValue(string key, object? value)
|
|
||||||
{
|
|
||||||
const string LOG_IDENT = "FastFlagManager::SetValue";
|
|
||||||
|
|
||||||
if (value is null)
|
|
||||||
{
|
|
||||||
if (Prop.ContainsKey(key))
|
|
||||||
App.Logger.WriteLine(LOG_IDENT, $"Deletion of '{key}' is pending");
|
|
||||||
|
|
||||||
Prop.Remove(key);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
if (Prop.ContainsKey(key))
|
|
||||||
{
|
|
||||||
if (key == Prop[key].ToString())
|
|
||||||
return;
|
|
||||||
|
|
||||||
App.Logger.WriteLine(LOG_IDENT, $"Changing of '{key}' from '{Prop[key]}' to '{value}' is pending");
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
App.Logger.WriteLine(LOG_IDENT, $"Setting of '{key}' to '{value}' is pending");
|
|
||||||
}
|
|
||||||
|
|
||||||
Prop[key] = value.ToString()!;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// this returns null if the fflag doesn't exist
|
|
||||||
public string? GetValue(string key)
|
|
||||||
{
|
|
||||||
// check if we have an updated change for it pushed first
|
|
||||||
if (Prop.TryGetValue(key, out object? value) && value is not null)
|
|
||||||
return value.ToString();
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void SetPreset(string prefix, object? value)
|
|
||||||
{
|
|
||||||
foreach (var pair in PresetFlags.Where(x => x.Key.StartsWith(prefix)))
|
|
||||||
SetValue(pair.Value, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void SetPresetEnum(string prefix, string target, object? value)
|
|
||||||
{
|
|
||||||
foreach (var pair in PresetFlags.Where(x => x.Key.StartsWith(prefix)))
|
|
||||||
{
|
|
||||||
if (pair.Key.StartsWith($"{prefix}.{target}"))
|
|
||||||
SetValue(pair.Value, value);
|
|
||||||
else
|
|
||||||
SetValue(pair.Value, null);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public string? GetPreset(string name)
|
|
||||||
{
|
|
||||||
if (!PresetFlags.ContainsKey(name))
|
|
||||||
{
|
|
||||||
App.Logger.WriteLine("FastFlagManager::GetPreset", $"Could not find preset {name}");
|
|
||||||
Debug.Assert(false, $"Could not find preset {name}");
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return GetValue(PresetFlags[name]);
|
|
||||||
}
|
|
||||||
|
|
||||||
public T GetPresetEnum<T>(IReadOnlyDictionary<T, string> mapping, string prefix, string value) where T : Enum
|
|
||||||
{
|
|
||||||
foreach (var pair in mapping)
|
|
||||||
{
|
|
||||||
if (pair.Value == "None")
|
|
||||||
continue;
|
|
||||||
|
|
||||||
if (GetPreset($"{prefix}.{pair.Value}") == value)
|
|
||||||
return pair.Key;
|
|
||||||
}
|
|
||||||
|
|
||||||
return mapping.First().Key;
|
|
||||||
}
|
|
||||||
|
|
||||||
public override void Save()
|
|
||||||
{
|
|
||||||
// convert all flag values to strings before saving
|
|
||||||
|
|
||||||
foreach (var pair in Prop)
|
|
||||||
Prop[pair.Key] = pair.Value.ToString()!;
|
|
||||||
|
|
||||||
base.Save();
|
|
||||||
|
|
||||||
// clone the dictionary
|
|
||||||
OriginalProp = new(Prop);
|
|
||||||
}
|
|
||||||
|
|
||||||
public override void Load(bool alertFailure = true)
|
|
||||||
{
|
|
||||||
base.Load(alertFailure);
|
|
||||||
|
|
||||||
// clone the dictionary
|
|
||||||
OriginalProp = new(Prop);
|
|
||||||
|
|
||||||
if (GetPreset("Network.Log") != "7")
|
|
||||||
SetPreset("Network.Log", "7");
|
|
||||||
|
|
||||||
if (GetPreset("Rendering.ManualFullscreen") != "False")
|
|
||||||
SetPreset("Rendering.ManualFullscreen", "False");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,7 +0,0 @@
|
|||||||
namespace Bloxstrap
|
|
||||||
{
|
|
||||||
public static class GlobalCache
|
|
||||||
{
|
|
||||||
public static readonly Dictionary<string, string?> ServerLocation = new();
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,32 +0,0 @@
|
|||||||
global using System;
|
|
||||||
global using System.Collections.Generic;
|
|
||||||
global using System.Diagnostics;
|
|
||||||
global using System.Globalization;
|
|
||||||
global using System.IO;
|
|
||||||
global using System.Text;
|
|
||||||
global using System.Text.Json;
|
|
||||||
global using System.Text.Json.Serialization;
|
|
||||||
global using System.Text.RegularExpressions;
|
|
||||||
global using System.Linq;
|
|
||||||
global using System.Net;
|
|
||||||
global using System.Net.Http;
|
|
||||||
global using System.Threading;
|
|
||||||
global using System.Threading.Tasks;
|
|
||||||
|
|
||||||
global using Bloxstrap.Enums;
|
|
||||||
global using Bloxstrap.Exceptions;
|
|
||||||
global using Bloxstrap.Extensions;
|
|
||||||
global using Bloxstrap.Models;
|
|
||||||
global using Bloxstrap.Models.APIs.Config;
|
|
||||||
global using Bloxstrap.Models.APIs.GitHub;
|
|
||||||
global using Bloxstrap.Models.APIs.Roblox;
|
|
||||||
global using Bloxstrap.Models.Attributes;
|
|
||||||
global using Bloxstrap.Models.BloxstrapRPC;
|
|
||||||
global using Bloxstrap.Models.Entities;
|
|
||||||
global using Bloxstrap.Models.Manifest;
|
|
||||||
global using Bloxstrap.Models.Persistable;
|
|
||||||
global using Bloxstrap.Models.SettingTasks;
|
|
||||||
global using Bloxstrap.Models.SettingTasks.Base;
|
|
||||||
global using Bloxstrap.Resources;
|
|
||||||
global using Bloxstrap.UI;
|
|
||||||
global using Bloxstrap.Utility;
|
|
@ -1,22 +0,0 @@
|
|||||||
namespace Bloxstrap
|
|
||||||
{
|
|
||||||
internal class HttpClientLoggingHandler : MessageProcessingHandler
|
|
||||||
{
|
|
||||||
public HttpClientLoggingHandler(HttpMessageHandler innerHandler)
|
|
||||||
: base(innerHandler)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override HttpRequestMessage ProcessRequest(HttpRequestMessage request, CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
App.Logger.WriteLine("HttpClientLoggingHandler::ProcessRequest", $"{request.Method} {request.RequestUri}");
|
|
||||||
return request;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override HttpResponseMessage ProcessResponse(HttpResponseMessage response, CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
App.Logger.WriteLine("HttpClientLoggingHandler::ProcessResponse", $"{(int)response.StatusCode} {response.ReasonPhrase} {response.RequestMessage!.RequestUri}");
|
|
||||||
return response;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,624 +0,0 @@
|
|||||||
using System.Windows;
|
|
||||||
using Microsoft.Win32;
|
|
||||||
|
|
||||||
namespace Bloxstrap
|
|
||||||
{
|
|
||||||
internal class Installer
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Should this version automatically open the release notes page?
|
|
||||||
/// Recommended for major updates only.
|
|
||||||
/// </summary>
|
|
||||||
private const bool OpenReleaseNotes = false;
|
|
||||||
|
|
||||||
private static string DesktopShortcut => Path.Combine(Paths.Desktop, $"{App.ProjectName}.lnk");
|
|
||||||
|
|
||||||
private static string StartMenuShortcut => Path.Combine(Paths.WindowsStartMenu, $"{App.ProjectName}.lnk");
|
|
||||||
|
|
||||||
public string InstallLocation = Path.Combine(Paths.LocalAppData, App.ProjectName);
|
|
||||||
|
|
||||||
public bool ExistingDataPresent => File.Exists(Path.Combine(InstallLocation, "Settings.json"));
|
|
||||||
|
|
||||||
public bool CreateDesktopShortcuts = true;
|
|
||||||
|
|
||||||
public bool CreateStartMenuShortcuts = true;
|
|
||||||
|
|
||||||
public bool EnableAnalytics = true;
|
|
||||||
|
|
||||||
public bool IsImplicitInstall = false;
|
|
||||||
|
|
||||||
public string InstallLocationError { get; set; } = "";
|
|
||||||
|
|
||||||
public void DoInstall()
|
|
||||||
{
|
|
||||||
const string LOG_IDENT = "Installer::DoInstall";
|
|
||||||
|
|
||||||
App.Logger.WriteLine(LOG_IDENT, "Beginning installation");
|
|
||||||
|
|
||||||
// should've been created earlier from the write test anyway
|
|
||||||
Directory.CreateDirectory(InstallLocation);
|
|
||||||
|
|
||||||
Paths.Initialize(InstallLocation);
|
|
||||||
|
|
||||||
if (!IsImplicitInstall)
|
|
||||||
{
|
|
||||||
Filesystem.AssertReadOnly(Paths.Application);
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
File.Copy(Paths.Process, Paths.Application, true);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
App.Logger.WriteLine(LOG_IDENT, "Could not overwrite executable");
|
|
||||||
App.Logger.WriteException(LOG_IDENT, ex);
|
|
||||||
|
|
||||||
Frontend.ShowMessageBox(Strings.Installer_Install_CannotOverwrite, MessageBoxImage.Error);
|
|
||||||
App.Terminate(ErrorCode.ERROR_INSTALL_FAILURE);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
using (var uninstallKey = Registry.CurrentUser.CreateSubKey(App.UninstallKey))
|
|
||||||
{
|
|
||||||
uninstallKey.SetValueSafe("DisplayIcon", $"{Paths.Application},0");
|
|
||||||
uninstallKey.SetValueSafe("DisplayName", App.ProjectName);
|
|
||||||
|
|
||||||
uninstallKey.SetValueSafe("DisplayVersion", App.Version);
|
|
||||||
|
|
||||||
if (uninstallKey.GetValue("InstallDate") is null)
|
|
||||||
uninstallKey.SetValueSafe("InstallDate", DateTime.Now.ToString("yyyyMMdd"));
|
|
||||||
|
|
||||||
uninstallKey.SetValueSafe("InstallLocation", Paths.Base);
|
|
||||||
uninstallKey.SetValueSafe("NoRepair", 1);
|
|
||||||
uninstallKey.SetValueSafe("Publisher", App.ProjectOwner);
|
|
||||||
uninstallKey.SetValueSafe("ModifyPath", $"\"{Paths.Application}\" -settings");
|
|
||||||
uninstallKey.SetValueSafe("QuietUninstallString", $"\"{Paths.Application}\" -uninstall -quiet");
|
|
||||||
uninstallKey.SetValueSafe("UninstallString", $"\"{Paths.Application}\" -uninstall");
|
|
||||||
uninstallKey.SetValueSafe("HelpLink", App.ProjectHelpLink);
|
|
||||||
uninstallKey.SetValueSafe("URLInfoAbout", App.ProjectSupportLink);
|
|
||||||
uninstallKey.SetValueSafe("URLUpdateInfo", App.ProjectDownloadLink);
|
|
||||||
}
|
|
||||||
|
|
||||||
// only register player, for the scenario where the user installs bloxstrap, closes it,
|
|
||||||
// and then launches from the website expecting it to work
|
|
||||||
// studio can be implicitly registered when it's first launched manually
|
|
||||||
WindowsRegistry.RegisterPlayer();
|
|
||||||
|
|
||||||
if (CreateDesktopShortcuts)
|
|
||||||
Shortcut.Create(Paths.Application, "", DesktopShortcut);
|
|
||||||
|
|
||||||
if (CreateStartMenuShortcuts)
|
|
||||||
Shortcut.Create(Paths.Application, "", StartMenuShortcut);
|
|
||||||
|
|
||||||
// existing configuration persisting from an earlier install
|
|
||||||
App.Settings.Load(false);
|
|
||||||
App.State.Load(false);
|
|
||||||
App.FastFlags.Load(false);
|
|
||||||
|
|
||||||
App.Settings.Prop.EnableAnalytics = EnableAnalytics;
|
|
||||||
|
|
||||||
if (App.IsStudioVisible)
|
|
||||||
WindowsRegistry.RegisterStudio();
|
|
||||||
|
|
||||||
App.Settings.Save();
|
|
||||||
|
|
||||||
App.Logger.WriteLine(LOG_IDENT, "Installation finished");
|
|
||||||
|
|
||||||
if (!IsImplicitInstall)
|
|
||||||
App.SendStat("installAction", "install");
|
|
||||||
}
|
|
||||||
|
|
||||||
private bool ValidateLocation()
|
|
||||||
{
|
|
||||||
// prevent from installing to the root of a drive
|
|
||||||
if (InstallLocation.Length <= 3)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
// unc path, just to be safe
|
|
||||||
if (InstallLocation.StartsWith("\\\\"))
|
|
||||||
return false;
|
|
||||||
|
|
||||||
if (InstallLocation.StartsWith(Path.GetTempPath(), StringComparison.InvariantCultureIgnoreCase)
|
|
||||||
|| InstallLocation.Contains("\\Temp\\", StringComparison.InvariantCultureIgnoreCase))
|
|
||||||
return false;
|
|
||||||
|
|
||||||
// prevent from installing to a onedrive folder
|
|
||||||
if (InstallLocation.Contains("OneDrive", StringComparison.InvariantCultureIgnoreCase))
|
|
||||||
return false;
|
|
||||||
|
|
||||||
// prevent from installing to an essential user profile folder (e.g. Documents, Downloads, Contacts idk)
|
|
||||||
if (String.Compare(Directory.GetParent(InstallLocation)?.FullName, Paths.UserProfile, StringComparison.InvariantCultureIgnoreCase) == 0)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
// prevent from installing into the program files folder
|
|
||||||
if (InstallLocation.Contains("Program Files"))
|
|
||||||
return false;
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool CheckInstallLocation()
|
|
||||||
{
|
|
||||||
if (string.IsNullOrEmpty(InstallLocation))
|
|
||||||
{
|
|
||||||
InstallLocationError = Strings.Menu_InstallLocation_NotSet;
|
|
||||||
}
|
|
||||||
else if (!ValidateLocation())
|
|
||||||
{
|
|
||||||
InstallLocationError = Strings.Menu_InstallLocation_CantInstall;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
if (!IsImplicitInstall
|
|
||||||
&& !InstallLocation.EndsWith(App.ProjectName, StringComparison.InvariantCultureIgnoreCase)
|
|
||||||
&& Directory.Exists(InstallLocation)
|
|
||||||
&& Directory.EnumerateFileSystemEntries(InstallLocation).Any())
|
|
||||||
{
|
|
||||||
string suggestedChange = Path.Combine(InstallLocation, App.ProjectName);
|
|
||||||
|
|
||||||
MessageBoxResult result = Frontend.ShowMessageBox(
|
|
||||||
String.Format(Strings.Menu_InstallLocation_NotEmpty, suggestedChange),
|
|
||||||
MessageBoxImage.Warning,
|
|
||||||
MessageBoxButton.YesNoCancel,
|
|
||||||
MessageBoxResult.Yes
|
|
||||||
);
|
|
||||||
|
|
||||||
if (result == MessageBoxResult.Yes)
|
|
||||||
InstallLocation = suggestedChange;
|
|
||||||
else if (result == MessageBoxResult.Cancel || result == MessageBoxResult.None)
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
// check if we can write to the directory (a bit hacky but eh)
|
|
||||||
string testFile = Path.Combine(InstallLocation, $"{App.ProjectName}WriteTest.txt");
|
|
||||||
|
|
||||||
Directory.CreateDirectory(InstallLocation);
|
|
||||||
File.WriteAllText(testFile, "");
|
|
||||||
File.Delete(testFile);
|
|
||||||
}
|
|
||||||
catch (UnauthorizedAccessException)
|
|
||||||
{
|
|
||||||
InstallLocationError = Strings.Menu_InstallLocation_NoWritePerms;
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
InstallLocationError = ex.Message;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return String.IsNullOrEmpty(InstallLocationError);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void DoUninstall(bool keepData)
|
|
||||||
{
|
|
||||||
const string LOG_IDENT = "Installer::DoUninstall";
|
|
||||||
|
|
||||||
var processes = new List<Process>();
|
|
||||||
|
|
||||||
if (!String.IsNullOrEmpty(App.State.Prop.Player.VersionGuid))
|
|
||||||
processes.AddRange(Process.GetProcessesByName(App.RobloxPlayerAppName));
|
|
||||||
|
|
||||||
if (App.IsStudioVisible)
|
|
||||||
processes.AddRange(Process.GetProcessesByName(App.RobloxStudioAppName));
|
|
||||||
|
|
||||||
// prompt to shutdown roblox if its currently running
|
|
||||||
if (processes.Any())
|
|
||||||
{
|
|
||||||
var result = Frontend.ShowMessageBox(
|
|
||||||
Strings.Bootstrapper_Uninstall_RobloxRunning,
|
|
||||||
MessageBoxImage.Information,
|
|
||||||
MessageBoxButton.OKCancel,
|
|
||||||
MessageBoxResult.OK
|
|
||||||
);
|
|
||||||
|
|
||||||
if (result != MessageBoxResult.OK)
|
|
||||||
{
|
|
||||||
App.Terminate(ErrorCode.ERROR_CANCELLED);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
foreach (var process in processes)
|
|
||||||
{
|
|
||||||
process.Kill();
|
|
||||||
process.Close();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
App.Logger.WriteLine(LOG_IDENT, $"Failed to close process! {ex}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
string robloxFolder = Path.Combine(Paths.LocalAppData, "Roblox");
|
|
||||||
bool playerStillInstalled = true;
|
|
||||||
bool studioStillInstalled = true;
|
|
||||||
|
|
||||||
// check if stock bootstrapper is still installed
|
|
||||||
using var playerKey = Registry.CurrentUser.OpenSubKey(@"Software\Microsoft\Windows\CurrentVersion\Uninstall\roblox-player");
|
|
||||||
var playerFolder = playerKey?.GetValue("InstallLocation");
|
|
||||||
|
|
||||||
if (playerKey is null || playerFolder is not string)
|
|
||||||
{
|
|
||||||
playerStillInstalled = false;
|
|
||||||
|
|
||||||
WindowsRegistry.Unregister("roblox");
|
|
||||||
WindowsRegistry.Unregister("roblox-player");
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
string playerPath = Path.Combine((string)playerFolder, "RobloxPlayerBeta.exe");
|
|
||||||
|
|
||||||
WindowsRegistry.RegisterPlayer(playerPath, "%1");
|
|
||||||
}
|
|
||||||
|
|
||||||
using var studioKey = Registry.CurrentUser.OpenSubKey(@"Software\Microsoft\Windows\CurrentVersion\Uninstall\roblox-studio");
|
|
||||||
var studioFolder = studioKey?.GetValue("InstallLocation");
|
|
||||||
|
|
||||||
if (studioKey is null || studioFolder is not string)
|
|
||||||
{
|
|
||||||
studioStillInstalled = false;
|
|
||||||
|
|
||||||
WindowsRegistry.Unregister("roblox-studio");
|
|
||||||
WindowsRegistry.Unregister("roblox-studio-auth");
|
|
||||||
|
|
||||||
WindowsRegistry.Unregister("Roblox.Place");
|
|
||||||
WindowsRegistry.Unregister(".rbxl");
|
|
||||||
WindowsRegistry.Unregister(".rbxlx");
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
string studioPath = Path.Combine((string)studioFolder, "RobloxStudioBeta.exe");
|
|
||||||
string studioLauncherPath = Path.Combine((string)studioFolder, "RobloxStudioLauncherBeta.exe");
|
|
||||||
|
|
||||||
WindowsRegistry.RegisterStudioProtocol(studioPath, "%1");
|
|
||||||
WindowsRegistry.RegisterStudioFileClass(studioPath, "-ide \"%1\"");
|
|
||||||
}
|
|
||||||
|
|
||||||
var cleanupSequence = new List<Action>
|
|
||||||
{
|
|
||||||
() =>
|
|
||||||
{
|
|
||||||
foreach (var file in Directory.GetFiles(Paths.Desktop).Where(x => x.EndsWith("lnk")))
|
|
||||||
{
|
|
||||||
var shortcut = ShellLink.Shortcut.ReadFromFile(file);
|
|
||||||
|
|
||||||
if (shortcut.ExtraData.EnvironmentVariableDataBlock?.TargetUnicode == Paths.Application)
|
|
||||||
File.Delete(file);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
() => File.Delete(StartMenuShortcut),
|
|
||||||
|
|
||||||
() => Directory.Delete(Paths.Versions, true),
|
|
||||||
() => Directory.Delete(Paths.Downloads, true),
|
|
||||||
|
|
||||||
() => File.Delete(App.State.FileLocation)
|
|
||||||
};
|
|
||||||
|
|
||||||
if (!keepData)
|
|
||||||
{
|
|
||||||
cleanupSequence.AddRange(new List<Action>
|
|
||||||
{
|
|
||||||
() => Directory.Delete(Paths.Modifications, true),
|
|
||||||
() => Directory.Delete(Paths.Logs, true),
|
|
||||||
|
|
||||||
() => File.Delete(App.Settings.FileLocation)
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
bool deleteFolder = Directory.GetFiles(Paths.Base).Length <= 3;
|
|
||||||
|
|
||||||
if (deleteFolder)
|
|
||||||
cleanupSequence.Add(() => Directory.Delete(Paths.Base, true));
|
|
||||||
|
|
||||||
if (!playerStillInstalled && !studioStillInstalled && Directory.Exists(robloxFolder))
|
|
||||||
cleanupSequence.Add(() => Directory.Delete(robloxFolder, true));
|
|
||||||
|
|
||||||
cleanupSequence.Add(() => Registry.CurrentUser.DeleteSubKey(App.UninstallKey));
|
|
||||||
|
|
||||||
foreach (var process in cleanupSequence)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
process();
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
App.Logger.WriteLine(LOG_IDENT, $"Encountered exception when running cleanup sequence (#{cleanupSequence.IndexOf(process)})");
|
|
||||||
App.Logger.WriteException(LOG_IDENT, ex);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Directory.Exists(Paths.Base))
|
|
||||||
{
|
|
||||||
// this is definitely one of the workaround hacks of all time
|
|
||||||
|
|
||||||
string deleteCommand;
|
|
||||||
|
|
||||||
if (deleteFolder)
|
|
||||||
deleteCommand = $"del /Q \"{Paths.Base}\\*\" && rmdir \"{Paths.Base}\"";
|
|
||||||
else
|
|
||||||
deleteCommand = $"del /Q \"{Paths.Application}\"";
|
|
||||||
|
|
||||||
Process.Start(new ProcessStartInfo()
|
|
||||||
{
|
|
||||||
FileName = "cmd.exe",
|
|
||||||
Arguments = $"/c timeout 5 && {deleteCommand}",
|
|
||||||
UseShellExecute = true,
|
|
||||||
WindowStyle = ProcessWindowStyle.Hidden
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
App.SendStat("installAction", "uninstall");
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void HandleUpgrade()
|
|
||||||
{
|
|
||||||
const string LOG_IDENT = "Installer::HandleUpgrade";
|
|
||||||
|
|
||||||
if (!File.Exists(Paths.Application) || Paths.Process == Paths.Application)
|
|
||||||
return;
|
|
||||||
|
|
||||||
// 2.0.0 downloads updates to <BaseFolder>/Updates so lol
|
|
||||||
bool isAutoUpgrade = App.LaunchSettings.UpgradeFlag.Active
|
|
||||||
|| Paths.Process.StartsWith(Path.Combine(Paths.Base, "Updates"))
|
|
||||||
|| Paths.Process.StartsWith(Path.Combine(Paths.LocalAppData, "Temp"))
|
|
||||||
|| Paths.Process.StartsWith(Paths.TempUpdates);
|
|
||||||
|
|
||||||
var existingVer = FileVersionInfo.GetVersionInfo(Paths.Application).ProductVersion;
|
|
||||||
var currentVer = FileVersionInfo.GetVersionInfo(Paths.Process).ProductVersion;
|
|
||||||
|
|
||||||
if (MD5Hash.FromFile(Paths.Process) == MD5Hash.FromFile(Paths.Application))
|
|
||||||
return;
|
|
||||||
|
|
||||||
if (currentVer is not null && existingVer is not null && Utilities.CompareVersions(currentVer, existingVer) == VersionComparison.LessThan)
|
|
||||||
{
|
|
||||||
var result = Frontend.ShowMessageBox(
|
|
||||||
Strings.InstallChecker_VersionLessThanInstalled,
|
|
||||||
MessageBoxImage.Question,
|
|
||||||
MessageBoxButton.YesNo
|
|
||||||
);
|
|
||||||
|
|
||||||
if (result != MessageBoxResult.Yes)
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// silently upgrade version if the command line flag is set or if we're launching from an auto update
|
|
||||||
if (!isAutoUpgrade)
|
|
||||||
{
|
|
||||||
var result = Frontend.ShowMessageBox(
|
|
||||||
Strings.InstallChecker_VersionDifferentThanInstalled,
|
|
||||||
MessageBoxImage.Question,
|
|
||||||
MessageBoxButton.YesNo
|
|
||||||
);
|
|
||||||
|
|
||||||
if (result != MessageBoxResult.Yes)
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
App.Logger.WriteLine(LOG_IDENT, "Doing upgrade");
|
|
||||||
|
|
||||||
Filesystem.AssertReadOnly(Paths.Application);
|
|
||||||
|
|
||||||
using (var ipl = new InterProcessLock("AutoUpdater", TimeSpan.FromSeconds(5)))
|
|
||||||
{
|
|
||||||
if (!ipl.IsAcquired)
|
|
||||||
{
|
|
||||||
App.Logger.WriteLine(LOG_IDENT, "Failed to update! (Could not obtain singleton mutex)");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// prior to 2.8.0, auto-updating was handled with this... bruteforce method
|
|
||||||
// now it's handled with the system mutex you see above, but we need to keep this logic for <2.8.0 versions
|
|
||||||
for (int i = 1; i <= 10; i++)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
File.Copy(Paths.Process, Paths.Application, true);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
if (i == 1)
|
|
||||||
{
|
|
||||||
App.Logger.WriteLine(LOG_IDENT, "Waiting for write permissions to update version");
|
|
||||||
}
|
|
||||||
else if (i == 10)
|
|
||||||
{
|
|
||||||
App.Logger.WriteLine(LOG_IDENT, "Failed to update! (Could not get write permissions after 10 tries/5 seconds)");
|
|
||||||
App.Logger.WriteException(LOG_IDENT, ex);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
Thread.Sleep(500);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
using (var uninstallKey = Registry.CurrentUser.CreateSubKey(App.UninstallKey))
|
|
||||||
{
|
|
||||||
uninstallKey.SetValueSafe("DisplayVersion", App.Version);
|
|
||||||
|
|
||||||
uninstallKey.SetValueSafe("Publisher", App.ProjectOwner);
|
|
||||||
uninstallKey.SetValueSafe("HelpLink", App.ProjectHelpLink);
|
|
||||||
uninstallKey.SetValueSafe("URLInfoAbout", App.ProjectSupportLink);
|
|
||||||
uninstallKey.SetValueSafe("URLUpdateInfo", App.ProjectDownloadLink);
|
|
||||||
}
|
|
||||||
|
|
||||||
// update migrations
|
|
||||||
|
|
||||||
if (existingVer is not null)
|
|
||||||
{
|
|
||||||
if (Utilities.CompareVersions(existingVer, "2.2.0") == VersionComparison.LessThan)
|
|
||||||
{
|
|
||||||
string path = Path.Combine(Paths.Integrations, "rbxfpsunlocker");
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
if (Directory.Exists(path))
|
|
||||||
Directory.Delete(path, true);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
App.Logger.WriteException(LOG_IDENT, ex);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Utilities.CompareVersions(existingVer, "2.3.0") == VersionComparison.LessThan)
|
|
||||||
{
|
|
||||||
string injectorLocation = Path.Combine(Paths.Modifications, "dxgi.dll");
|
|
||||||
string configLocation = Path.Combine(Paths.Modifications, "ReShade.ini");
|
|
||||||
|
|
||||||
if (File.Exists(injectorLocation))
|
|
||||||
File.Delete(injectorLocation);
|
|
||||||
|
|
||||||
if (File.Exists(configLocation))
|
|
||||||
File.Delete(configLocation);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
if (Utilities.CompareVersions(existingVer, "2.5.0") == VersionComparison.LessThan)
|
|
||||||
{
|
|
||||||
App.FastFlags.SetValue("DFFlagDisableDPIScale", null);
|
|
||||||
App.FastFlags.SetValue("DFFlagVariableDPIScale2", null);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Utilities.CompareVersions(existingVer, "2.6.0") == VersionComparison.LessThan)
|
|
||||||
{
|
|
||||||
if (App.Settings.Prop.UseDisableAppPatch)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
File.Delete(Path.Combine(Paths.Modifications, "ExtraContent\\places\\Mobile.rbxl"));
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
App.Logger.WriteException(LOG_IDENT, ex);
|
|
||||||
}
|
|
||||||
|
|
||||||
App.Settings.Prop.EnableActivityTracking = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (App.Settings.Prop.BootstrapperStyle == BootstrapperStyle.ClassicFluentDialog)
|
|
||||||
App.Settings.Prop.BootstrapperStyle = BootstrapperStyle.FluentDialog;
|
|
||||||
|
|
||||||
_ = int.TryParse(App.FastFlags.GetPreset("Rendering.Framerate"), out int x);
|
|
||||||
if (x == 0)
|
|
||||||
App.FastFlags.SetPreset("Rendering.Framerate", null);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Utilities.CompareVersions(existingVer, "2.8.0") == VersionComparison.LessThan)
|
|
||||||
{
|
|
||||||
if (isAutoUpgrade)
|
|
||||||
{
|
|
||||||
if (App.LaunchSettings.Args.Length == 0)
|
|
||||||
App.LaunchSettings.RobloxLaunchMode = LaunchMode.Player;
|
|
||||||
|
|
||||||
string? query = App.LaunchSettings.Args.FirstOrDefault(x => x.Contains("roblox"));
|
|
||||||
|
|
||||||
if (query is not null)
|
|
||||||
{
|
|
||||||
App.LaunchSettings.RobloxLaunchMode = LaunchMode.Player;
|
|
||||||
App.LaunchSettings.RobloxLaunchArgs = query;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
string oldDesktopPath = Path.Combine(Paths.Desktop, "Play Roblox.lnk");
|
|
||||||
string oldStartPath = Path.Combine(Paths.WindowsStartMenu, "Bloxstrap");
|
|
||||||
|
|
||||||
if (File.Exists(oldDesktopPath))
|
|
||||||
File.Move(oldDesktopPath, DesktopShortcut, true);
|
|
||||||
|
|
||||||
if (Directory.Exists(oldStartPath))
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
Directory.Delete(oldStartPath, true);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
App.Logger.WriteException(LOG_IDENT, ex);
|
|
||||||
}
|
|
||||||
|
|
||||||
Shortcut.Create(Paths.Application, "", StartMenuShortcut);
|
|
||||||
}
|
|
||||||
|
|
||||||
Registry.CurrentUser.DeleteSubKeyTree("Software\\Bloxstrap", false);
|
|
||||||
|
|
||||||
WindowsRegistry.RegisterPlayer();
|
|
||||||
|
|
||||||
App.FastFlags.SetValue("FFlagDisableNewIGMinDUA", null);
|
|
||||||
App.FastFlags.SetValue("FFlagFixGraphicsQuality", null);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Utilities.CompareVersions(existingVer, "2.8.1") == VersionComparison.LessThan)
|
|
||||||
{
|
|
||||||
// wipe all escape menu flag presets
|
|
||||||
App.FastFlags.SetValue("FIntNewInGameMenuPercentRollout3", null);
|
|
||||||
App.FastFlags.SetValue("FFlagEnableInGameMenuControls", null);
|
|
||||||
App.FastFlags.SetValue("FFlagEnableInGameMenuModernization", null);
|
|
||||||
App.FastFlags.SetValue("FFlagEnableInGameMenuChrome", null);
|
|
||||||
App.FastFlags.SetValue("FFlagFixReportButtonCutOff", null);
|
|
||||||
App.FastFlags.SetValue("FFlagEnableMenuControlsABTest", null);
|
|
||||||
App.FastFlags.SetValue("FFlagEnableV3MenuABTest3", null);
|
|
||||||
App.FastFlags.SetValue("FFlagEnableInGameMenuChromeABTest3", null);
|
|
||||||
App.FastFlags.SetValue("FFlagEnableInGameMenuChromeABTest4", null);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Utilities.CompareVersions(existingVer, "2.8.2") == VersionComparison.LessThan)
|
|
||||||
{
|
|
||||||
string robloxDirectory = Path.Combine(Paths.Base, "Roblox");
|
|
||||||
|
|
||||||
if (Directory.Exists(robloxDirectory))
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
Directory.Delete(robloxDirectory, true);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
App.Logger.WriteLine(LOG_IDENT, "Failed to delete the Roblox directory");
|
|
||||||
App.Logger.WriteException(LOG_IDENT, ex);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Utilities.CompareVersions(existingVer, "2.8.3") == VersionComparison.LessThan)
|
|
||||||
{
|
|
||||||
// force reinstallation
|
|
||||||
App.State.Prop.Player.VersionGuid = "";
|
|
||||||
App.State.Prop.Studio.VersionGuid = "";
|
|
||||||
}
|
|
||||||
|
|
||||||
App.Settings.Save();
|
|
||||||
App.FastFlags.Save();
|
|
||||||
App.State.Save();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (currentVer is null)
|
|
||||||
return;
|
|
||||||
|
|
||||||
App.SendStat("installAction", "upgrade");
|
|
||||||
|
|
||||||
if (isAutoUpgrade)
|
|
||||||
{
|
|
||||||
#pragma warning disable CS0162 // Unreachable code detected
|
|
||||||
if (OpenReleaseNotes)
|
|
||||||
Utilities.ShellExecute($"https://github.com/{App.ProjectRepository}/wiki/Release-notes-for-Bloxstrap-v{currentVer}");
|
|
||||||
#pragma warning restore CS0162 // Unreachable code detected
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
Frontend.ShowMessageBox(
|
|
||||||
string.Format(Strings.InstallChecker_Updated, currentVer),
|
|
||||||
MessageBoxImage.Information,
|
|
||||||
MessageBoxButton.OK
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,389 +0,0 @@
|
|||||||
namespace Bloxstrap.Integrations
|
|
||||||
{
|
|
||||||
public class ActivityWatcher : IDisposable
|
|
||||||
{
|
|
||||||
private const string GameMessageEntry = "[FLog::Output] [BloxstrapRPC]";
|
|
||||||
private const string GameJoiningEntry = "[FLog::Output] ! Joining game";
|
|
||||||
|
|
||||||
// these entries are technically volatile!
|
|
||||||
// they only get printed depending on their configured FLog level, which could change at any time
|
|
||||||
// while levels being changed is fairly rare, please limit the number of varying number of FLog types you have to use, if possible
|
|
||||||
|
|
||||||
private const string GameTeleportingEntry = "[FLog::GameJoinUtil] GameJoinUtil::initiateTeleportToPlace";
|
|
||||||
private const string GameJoiningPrivateServerEntry = "[FLog::GameJoinUtil] GameJoinUtil::joinGamePostPrivateServer";
|
|
||||||
private const string GameJoiningReservedServerEntry = "[FLog::GameJoinUtil] GameJoinUtil::initiateTeleportToReservedServer";
|
|
||||||
private const string GameJoiningUniverseEntry = "[FLog::GameJoinLoadTime] Report game_join_loadtime:";
|
|
||||||
private const string GameJoiningUDMUXEntry = "[FLog::Network] UDMUX Address = ";
|
|
||||||
private const string GameJoinedEntry = "[FLog::Network] serverId:";
|
|
||||||
private const string GameDisconnectedEntry = "[FLog::Network] Time to disconnect replication data:";
|
|
||||||
private const string GameLeavingEntry = "[FLog::SingleSurfaceApp] leaveUGCGameInternal";
|
|
||||||
|
|
||||||
private const string GameJoiningEntryPattern = @"! Joining game '([0-9a-f\-]{36})' place ([0-9]+) at ([0-9\.]+)";
|
|
||||||
private const string GameJoiningPrivateServerPattern = @"""accessCode"":""([0-9a-f\-]{36})""";
|
|
||||||
private const string GameJoiningUniversePattern = @"universeid:([0-9]+).*userid:([0-9]+)";
|
|
||||||
private const string GameJoiningUDMUXPattern = @"UDMUX Address = ([0-9\.]+), Port = [0-9]+ \| RCC Server Address = ([0-9\.]+), Port = [0-9]+";
|
|
||||||
private const string GameJoinedEntryPattern = @"serverId: ([0-9\.]+)\|[0-9]+";
|
|
||||||
private const string GameMessageEntryPattern = @"\[BloxstrapRPC\] (.*)";
|
|
||||||
|
|
||||||
private int _logEntriesRead = 0;
|
|
||||||
private bool _teleportMarker = false;
|
|
||||||
private bool _reservedTeleportMarker = false;
|
|
||||||
|
|
||||||
public event EventHandler<string>? OnLogEntry;
|
|
||||||
public event EventHandler? OnGameJoin;
|
|
||||||
public event EventHandler? OnGameLeave;
|
|
||||||
public event EventHandler? OnLogOpen;
|
|
||||||
public event EventHandler? OnAppClose;
|
|
||||||
public event EventHandler<Message>? OnRPCMessage;
|
|
||||||
|
|
||||||
private DateTime LastRPCRequest;
|
|
||||||
|
|
||||||
public string LogLocation = null!;
|
|
||||||
|
|
||||||
public bool InGame = false;
|
|
||||||
|
|
||||||
public ActivityData Data { get; private set; } = new();
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Ordered by newest to oldest
|
|
||||||
/// </summary>
|
|
||||||
public List<ActivityData> History = new();
|
|
||||||
|
|
||||||
public bool IsDisposed = false;
|
|
||||||
|
|
||||||
public ActivityWatcher(string? logFile = null)
|
|
||||||
{
|
|
||||||
if (!String.IsNullOrEmpty(logFile))
|
|
||||||
LogLocation = logFile;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async void Start()
|
|
||||||
{
|
|
||||||
const string LOG_IDENT = "ActivityWatcher::Start";
|
|
||||||
|
|
||||||
// okay, here's the process:
|
|
||||||
//
|
|
||||||
// - tail the latest log file from %localappdata%\roblox\logs
|
|
||||||
// - check for specific lines to determine player's game activity as shown below:
|
|
||||||
//
|
|
||||||
// - get the place id, job id and machine address from '! Joining game '{{JOBID}}' place {{PLACEID}} at {{MACHINEADDRESS}}' entry
|
|
||||||
// - confirm place join with 'serverId: {{MACHINEADDRESS}}|{{MACHINEPORT}}' entry
|
|
||||||
// - check for leaves/disconnects with 'Time to disconnect replication data: {{TIME}}' entry
|
|
||||||
//
|
|
||||||
// we'll tail the log file continuously, monitoring for any log entries that we need to determine the current game activity
|
|
||||||
|
|
||||||
FileInfo logFileInfo;
|
|
||||||
|
|
||||||
if (String.IsNullOrEmpty(LogLocation))
|
|
||||||
{
|
|
||||||
string logDirectory = Path.Combine(Paths.LocalAppData, "Roblox\\logs");
|
|
||||||
|
|
||||||
if (!Directory.Exists(logDirectory))
|
|
||||||
return;
|
|
||||||
|
|
||||||
// we need to make sure we're fetching the absolute latest log file
|
|
||||||
// if roblox doesn't start quickly enough, we can wind up fetching the previous log file
|
|
||||||
// good rule of thumb is to find a log file that was created in the last 15 seconds or so
|
|
||||||
|
|
||||||
App.Logger.WriteLine(LOG_IDENT, "Opening Roblox log file...");
|
|
||||||
|
|
||||||
while (true)
|
|
||||||
{
|
|
||||||
logFileInfo = new DirectoryInfo(logDirectory)
|
|
||||||
.GetFiles()
|
|
||||||
.Where(x => x.Name.Contains("Player", StringComparison.OrdinalIgnoreCase) && x.CreationTime <= DateTime.Now)
|
|
||||||
.OrderByDescending(x => x.CreationTime)
|
|
||||||
.First();
|
|
||||||
|
|
||||||
if (logFileInfo.CreationTime.AddSeconds(15) > DateTime.Now)
|
|
||||||
break;
|
|
||||||
|
|
||||||
App.Logger.WriteLine(LOG_IDENT, $"Could not find recent enough log file, waiting... (newest is {logFileInfo.Name})");
|
|
||||||
await Task.Delay(1000);
|
|
||||||
}
|
|
||||||
|
|
||||||
LogLocation = logFileInfo.FullName;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
logFileInfo = new FileInfo(LogLocation);
|
|
||||||
}
|
|
||||||
|
|
||||||
OnLogOpen?.Invoke(this, EventArgs.Empty);
|
|
||||||
|
|
||||||
var logFileStream = logFileInfo.Open(FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
|
|
||||||
|
|
||||||
App.Logger.WriteLine(LOG_IDENT, $"Opened {LogLocation}");
|
|
||||||
|
|
||||||
using var streamReader = new StreamReader(logFileStream);
|
|
||||||
|
|
||||||
while (!IsDisposed)
|
|
||||||
{
|
|
||||||
string? log = await streamReader.ReadLineAsync();
|
|
||||||
|
|
||||||
if (log is null)
|
|
||||||
await Task.Delay(1000);
|
|
||||||
else
|
|
||||||
ReadLogEntry(log);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void ReadLogEntry(string entry)
|
|
||||||
{
|
|
||||||
const string LOG_IDENT = "ActivityWatcher::ReadLogEntry";
|
|
||||||
|
|
||||||
OnLogEntry?.Invoke(this, entry);
|
|
||||||
|
|
||||||
_logEntriesRead += 1;
|
|
||||||
|
|
||||||
// debug stats to ensure that the log reader is working correctly
|
|
||||||
// if more than 1000 log entries have been read, only log per 100 to save on spam
|
|
||||||
if (_logEntriesRead <= 1000 && _logEntriesRead % 50 == 0)
|
|
||||||
App.Logger.WriteLine(LOG_IDENT, $"Read {_logEntriesRead} log entries");
|
|
||||||
else if (_logEntriesRead % 100 == 0)
|
|
||||||
App.Logger.WriteLine(LOG_IDENT, $"Read {_logEntriesRead} log entries");
|
|
||||||
|
|
||||||
if (entry.Contains(GameLeavingEntry))
|
|
||||||
{
|
|
||||||
App.Logger.WriteLine(LOG_IDENT, "User is back into the desktop app");
|
|
||||||
|
|
||||||
OnAppClose?.Invoke(this, EventArgs.Empty);
|
|
||||||
|
|
||||||
if (Data.PlaceId != 0 && !InGame)
|
|
||||||
{
|
|
||||||
App.Logger.WriteLine(LOG_IDENT, "User appears to be leaving from a cancelled/errored join");
|
|
||||||
Data = new();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!InGame && Data.PlaceId == 0)
|
|
||||||
{
|
|
||||||
// We are not in a game, nor are in the process of joining one
|
|
||||||
|
|
||||||
if (entry.Contains(GameJoiningPrivateServerEntry))
|
|
||||||
{
|
|
||||||
// we only expect to be joining a private server if we're not already in a game
|
|
||||||
|
|
||||||
Data.ServerType = ServerType.Private;
|
|
||||||
|
|
||||||
var match = Regex.Match(entry, GameJoiningPrivateServerPattern);
|
|
||||||
|
|
||||||
if (match.Groups.Count != 2)
|
|
||||||
{
|
|
||||||
App.Logger.WriteLine(LOG_IDENT, "Failed to assert format for game join private server entry");
|
|
||||||
App.Logger.WriteLine(LOG_IDENT, entry);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
Data.AccessCode = match.Groups[1].Value;
|
|
||||||
}
|
|
||||||
else if (entry.Contains(GameJoiningEntry))
|
|
||||||
{
|
|
||||||
Match match = Regex.Match(entry, GameJoiningEntryPattern);
|
|
||||||
|
|
||||||
if (match.Groups.Count != 4)
|
|
||||||
{
|
|
||||||
App.Logger.WriteLine(LOG_IDENT, $"Failed to assert format for game join entry");
|
|
||||||
App.Logger.WriteLine(LOG_IDENT, entry);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
InGame = false;
|
|
||||||
Data.PlaceId = long.Parse(match.Groups[2].Value);
|
|
||||||
Data.JobId = match.Groups[1].Value;
|
|
||||||
Data.MachineAddress = match.Groups[3].Value;
|
|
||||||
|
|
||||||
if (App.Settings.Prop.ShowServerDetails && Data.MachineAddressValid)
|
|
||||||
_ = Data.QueryServerLocation();
|
|
||||||
|
|
||||||
if (_teleportMarker)
|
|
||||||
{
|
|
||||||
Data.IsTeleport = true;
|
|
||||||
_teleportMarker = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (_reservedTeleportMarker)
|
|
||||||
{
|
|
||||||
Data.ServerType = ServerType.Reserved;
|
|
||||||
_reservedTeleportMarker = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
App.Logger.WriteLine(LOG_IDENT, $"Joining Game ({Data})");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if (!InGame && Data.PlaceId != 0)
|
|
||||||
{
|
|
||||||
// We are not confirmed to be in a game, but we are in the process of joining one
|
|
||||||
|
|
||||||
if (entry.Contains(GameJoiningUniverseEntry))
|
|
||||||
{
|
|
||||||
var match = Regex.Match(entry, GameJoiningUniversePattern);
|
|
||||||
|
|
||||||
if (match.Groups.Count != 3)
|
|
||||||
{
|
|
||||||
App.Logger.WriteLine(LOG_IDENT, "Failed to assert format for game join universe entry");
|
|
||||||
App.Logger.WriteLine(LOG_IDENT, entry);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
Data.UniverseId = Int64.Parse(match.Groups[1].Value);
|
|
||||||
Data.UserId = Int64.Parse(match.Groups[2].Value);
|
|
||||||
|
|
||||||
if (History.Any())
|
|
||||||
{
|
|
||||||
var lastActivity = History.First();
|
|
||||||
|
|
||||||
if (Data.UniverseId == lastActivity.UniverseId && Data.IsTeleport)
|
|
||||||
Data.RootActivity = lastActivity.RootActivity ?? lastActivity;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if (entry.Contains(GameJoiningUDMUXEntry))
|
|
||||||
{
|
|
||||||
var match = Regex.Match(entry, GameJoiningUDMUXPattern);
|
|
||||||
|
|
||||||
if (match.Groups.Count != 3 || match.Groups[2].Value != Data.MachineAddress)
|
|
||||||
{
|
|
||||||
App.Logger.WriteLine(LOG_IDENT, "Failed to assert format for game join UDMUX entry");
|
|
||||||
App.Logger.WriteLine(LOG_IDENT, entry);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
Data.MachineAddress = match.Groups[1].Value;
|
|
||||||
|
|
||||||
if (App.Settings.Prop.ShowServerDetails)
|
|
||||||
_ = Data.QueryServerLocation();
|
|
||||||
|
|
||||||
App.Logger.WriteLine(LOG_IDENT, $"Server is UDMUX protected ({Data})");
|
|
||||||
}
|
|
||||||
else if (entry.Contains(GameJoinedEntry))
|
|
||||||
{
|
|
||||||
Match match = Regex.Match(entry, GameJoinedEntryPattern);
|
|
||||||
|
|
||||||
if (match.Groups.Count != 2 || match.Groups[1].Value != Data.MachineAddress)
|
|
||||||
{
|
|
||||||
App.Logger.WriteLine(LOG_IDENT, $"Failed to assert format for game joined entry");
|
|
||||||
App.Logger.WriteLine(LOG_IDENT, entry);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
App.Logger.WriteLine(LOG_IDENT, $"Joined Game ({Data})");
|
|
||||||
|
|
||||||
InGame = true;
|
|
||||||
Data.TimeJoined = DateTime.Now;
|
|
||||||
|
|
||||||
OnGameJoin?.Invoke(this, EventArgs.Empty);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if (InGame && Data.PlaceId != 0)
|
|
||||||
{
|
|
||||||
// We are confirmed to be in a game
|
|
||||||
|
|
||||||
if (entry.Contains(GameDisconnectedEntry))
|
|
||||||
{
|
|
||||||
App.Logger.WriteLine(LOG_IDENT, $"Disconnected from Game ({Data})");
|
|
||||||
|
|
||||||
Data.TimeLeft = DateTime.Now;
|
|
||||||
History.Insert(0, Data);
|
|
||||||
|
|
||||||
InGame = false;
|
|
||||||
Data = new();
|
|
||||||
|
|
||||||
OnGameLeave?.Invoke(this, EventArgs.Empty);
|
|
||||||
}
|
|
||||||
else if (entry.Contains(GameTeleportingEntry))
|
|
||||||
{
|
|
||||||
App.Logger.WriteLine(LOG_IDENT, $"Initiating teleport to server ({Data})");
|
|
||||||
_teleportMarker = true;
|
|
||||||
}
|
|
||||||
else if (entry.Contains(GameJoiningReservedServerEntry))
|
|
||||||
{
|
|
||||||
_teleportMarker = true;
|
|
||||||
_reservedTeleportMarker = true;
|
|
||||||
}
|
|
||||||
else if (entry.Contains(GameMessageEntry))
|
|
||||||
{
|
|
||||||
var match = Regex.Match(entry, GameMessageEntryPattern);
|
|
||||||
|
|
||||||
if (match.Groups.Count != 2)
|
|
||||||
{
|
|
||||||
App.Logger.WriteLine(LOG_IDENT, $"Failed to assert format for RPC message entry");
|
|
||||||
App.Logger.WriteLine(LOG_IDENT, entry);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
string messagePlain = match.Groups[1].Value;
|
|
||||||
Message? message;
|
|
||||||
|
|
||||||
App.Logger.WriteLine(LOG_IDENT, $"Received message: '{messagePlain}'");
|
|
||||||
|
|
||||||
if ((DateTime.Now - LastRPCRequest).TotalSeconds <= 1)
|
|
||||||
{
|
|
||||||
App.Logger.WriteLine(LOG_IDENT, "Dropping message as ratelimit has been hit");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
message = JsonSerializer.Deserialize<Message>(messagePlain);
|
|
||||||
}
|
|
||||||
catch (Exception)
|
|
||||||
{
|
|
||||||
App.Logger.WriteLine(LOG_IDENT, "Failed to parse message! (JSON deserialization threw an exception)");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (message is null)
|
|
||||||
{
|
|
||||||
App.Logger.WriteLine(LOG_IDENT, "Failed to parse message! (JSON deserialization returned null)");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(message.Command))
|
|
||||||
{
|
|
||||||
App.Logger.WriteLine(LOG_IDENT, "Failed to parse message! (Command is empty)");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (message.Command == "SetLaunchData")
|
|
||||||
{
|
|
||||||
string? data;
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
data = message.Data.Deserialize<string>();
|
|
||||||
}
|
|
||||||
catch (Exception)
|
|
||||||
{
|
|
||||||
App.Logger.WriteLine(LOG_IDENT, "Failed to parse message! (JSON deserialization threw an exception)");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (data is null)
|
|
||||||
{
|
|
||||||
App.Logger.WriteLine(LOG_IDENT, "Failed to parse message! (JSON deserialization returned null)");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (data.Length > 200)
|
|
||||||
{
|
|
||||||
App.Logger.WriteLine(LOG_IDENT, "Data cannot be longer than 200 characters");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
Data.RPCLaunchData = data;
|
|
||||||
}
|
|
||||||
|
|
||||||
OnRPCMessage?.Invoke(this, message);
|
|
||||||
|
|
||||||
LastRPCRequest = DateTime.Now;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Dispose()
|
|
||||||
{
|
|
||||||
IsDisposed = true;
|
|
||||||
GC.SuppressFinalize(this);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,344 +0,0 @@
|
|||||||
using System.Windows;
|
|
||||||
using Bloxstrap.Models.RobloxApi;
|
|
||||||
using DiscordRPC;
|
|
||||||
|
|
||||||
namespace Bloxstrap.Integrations
|
|
||||||
{
|
|
||||||
public class DiscordRichPresence : IDisposable
|
|
||||||
{
|
|
||||||
private readonly DiscordRpcClient _rpcClient = new("1005469189907173486");
|
|
||||||
private readonly ActivityWatcher _activityWatcher;
|
|
||||||
private readonly Queue<Message> _messageQueue = new();
|
|
||||||
|
|
||||||
private DiscordRPC.RichPresence? _currentPresence;
|
|
||||||
private DiscordRPC.RichPresence? _originalPresence;
|
|
||||||
|
|
||||||
private bool _visible = true;
|
|
||||||
|
|
||||||
public DiscordRichPresence(ActivityWatcher activityWatcher)
|
|
||||||
{
|
|
||||||
const string LOG_IDENT = "DiscordRichPresence";
|
|
||||||
|
|
||||||
_activityWatcher = activityWatcher;
|
|
||||||
|
|
||||||
_activityWatcher.OnGameJoin += (_, _) => Task.Run(() => SetCurrentGame());
|
|
||||||
_activityWatcher.OnGameLeave += (_, _) => Task.Run(() => SetCurrentGame());
|
|
||||||
_activityWatcher.OnRPCMessage += (_, message) => ProcessRPCMessage(message);
|
|
||||||
|
|
||||||
_rpcClient.OnReady += (_, e) =>
|
|
||||||
App.Logger.WriteLine(LOG_IDENT, $"Received ready from user {e.User} ({e.User.ID})");
|
|
||||||
|
|
||||||
_rpcClient.OnPresenceUpdate += (_, e) =>
|
|
||||||
App.Logger.WriteLine(LOG_IDENT, "Presence updated");
|
|
||||||
|
|
||||||
_rpcClient.OnError += (_, e) =>
|
|
||||||
App.Logger.WriteLine(LOG_IDENT, $"An RPC error occurred - {e.Message}");
|
|
||||||
|
|
||||||
_rpcClient.OnConnectionEstablished += (_, e) =>
|
|
||||||
App.Logger.WriteLine(LOG_IDENT, "Established connection with Discord RPC");
|
|
||||||
|
|
||||||
//spams log as it tries to connect every ~15 sec when discord is closed so not now
|
|
||||||
//_rpcClient.OnConnectionFailed += (_, e) =>
|
|
||||||
// App.Logger.WriteLine(LOG_IDENT, "Failed to establish connection with Discord RPC");
|
|
||||||
|
|
||||||
_rpcClient.OnClose += (_, e) =>
|
|
||||||
App.Logger.WriteLine(LOG_IDENT, $"Lost connection to Discord RPC - {e.Reason} ({e.Code})");
|
|
||||||
|
|
||||||
_rpcClient.Initialize();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void ProcessRPCMessage(Message message, bool implicitUpdate = true)
|
|
||||||
{
|
|
||||||
const string LOG_IDENT = "DiscordRichPresence::ProcessRPCMessage";
|
|
||||||
|
|
||||||
if (message.Command != "SetRichPresence" && message.Command != "SetLaunchData")
|
|
||||||
return;
|
|
||||||
|
|
||||||
if (_currentPresence is null || _originalPresence is null)
|
|
||||||
{
|
|
||||||
App.Logger.WriteLine(LOG_IDENT, "Presence is not set, enqueuing message");
|
|
||||||
_messageQueue.Enqueue(message);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// a lot of repeated code here, could this somehow be cleaned up?
|
|
||||||
|
|
||||||
if (message.Command == "SetLaunchData")
|
|
||||||
{
|
|
||||||
_currentPresence.Buttons = GetButtons();
|
|
||||||
}
|
|
||||||
else if (message.Command == "SetRichPresence")
|
|
||||||
{
|
|
||||||
Models.BloxstrapRPC.RichPresence? presenceData;
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
presenceData = message.Data.Deserialize<Models.BloxstrapRPC.RichPresence>();
|
|
||||||
}
|
|
||||||
catch (Exception)
|
|
||||||
{
|
|
||||||
App.Logger.WriteLine(LOG_IDENT, "Failed to parse message! (JSON deserialization threw an exception)");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (presenceData is null)
|
|
||||||
{
|
|
||||||
App.Logger.WriteLine(LOG_IDENT, "Failed to parse message! (JSON deserialization returned null)");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (presenceData.Details is not null)
|
|
||||||
{
|
|
||||||
if (presenceData.Details.Length > 128)
|
|
||||||
App.Logger.WriteLine(LOG_IDENT, $"Details cannot be longer than 128 characters");
|
|
||||||
else if (presenceData.Details == "<reset>")
|
|
||||||
_currentPresence.Details = _originalPresence.Details;
|
|
||||||
else
|
|
||||||
_currentPresence.Details = presenceData.Details;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (presenceData.State is not null)
|
|
||||||
{
|
|
||||||
if (presenceData.State.Length > 128)
|
|
||||||
App.Logger.WriteLine(LOG_IDENT, $"State cannot be longer than 128 characters");
|
|
||||||
else if (presenceData.State == "<reset>")
|
|
||||||
_currentPresence.State = _originalPresence.State;
|
|
||||||
else
|
|
||||||
_currentPresence.State = presenceData.State;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (presenceData.TimestampStart == 0)
|
|
||||||
_currentPresence.Timestamps.Start = null;
|
|
||||||
else if (presenceData.TimestampStart is not null)
|
|
||||||
_currentPresence.Timestamps.StartUnixMilliseconds = presenceData.TimestampStart * 1000;
|
|
||||||
|
|
||||||
if (presenceData.TimestampEnd == 0)
|
|
||||||
_currentPresence.Timestamps.End = null;
|
|
||||||
else if (presenceData.TimestampEnd is not null)
|
|
||||||
_currentPresence.Timestamps.EndUnixMilliseconds = presenceData.TimestampEnd * 1000;
|
|
||||||
|
|
||||||
if (presenceData.SmallImage is not null)
|
|
||||||
{
|
|
||||||
if (presenceData.SmallImage.Clear)
|
|
||||||
{
|
|
||||||
_currentPresence.Assets.SmallImageKey = "";
|
|
||||||
}
|
|
||||||
else if (presenceData.SmallImage.Reset)
|
|
||||||
{
|
|
||||||
_currentPresence.Assets.SmallImageText = _originalPresence.Assets.SmallImageText;
|
|
||||||
_currentPresence.Assets.SmallImageKey = _originalPresence.Assets.SmallImageKey;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
if (presenceData.SmallImage.AssetId is not null)
|
|
||||||
_currentPresence.Assets.SmallImageKey = $"https://assetdelivery.roblox.com/v1/asset/?id={presenceData.SmallImage.AssetId}";
|
|
||||||
|
|
||||||
if (presenceData.SmallImage.HoverText is not null)
|
|
||||||
_currentPresence.Assets.SmallImageText = presenceData.SmallImage.HoverText;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (presenceData.LargeImage is not null)
|
|
||||||
{
|
|
||||||
if (presenceData.LargeImage.Clear)
|
|
||||||
{
|
|
||||||
_currentPresence.Assets.LargeImageKey = "";
|
|
||||||
}
|
|
||||||
else if (presenceData.LargeImage.Reset)
|
|
||||||
{
|
|
||||||
_currentPresence.Assets.LargeImageText = _originalPresence.Assets.LargeImageText;
|
|
||||||
_currentPresence.Assets.LargeImageKey = _originalPresence.Assets.LargeImageKey;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
if (presenceData.LargeImage.AssetId is not null)
|
|
||||||
_currentPresence.Assets.LargeImageKey = $"https://assetdelivery.roblox.com/v1/asset/?id={presenceData.LargeImage.AssetId}";
|
|
||||||
|
|
||||||
if (presenceData.LargeImage.HoverText is not null)
|
|
||||||
_currentPresence.Assets.LargeImageText = presenceData.LargeImage.HoverText;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (implicitUpdate)
|
|
||||||
UpdatePresence();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void SetVisibility(bool visible)
|
|
||||||
{
|
|
||||||
App.Logger.WriteLine("DiscordRichPresence::SetVisibility", $"Setting presence visibility ({visible})");
|
|
||||||
|
|
||||||
_visible = visible;
|
|
||||||
|
|
||||||
if (_visible)
|
|
||||||
UpdatePresence();
|
|
||||||
else
|
|
||||||
_rpcClient.ClearPresence();
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<bool> SetCurrentGame()
|
|
||||||
{
|
|
||||||
const string LOG_IDENT = "DiscordRichPresence::SetCurrentGame";
|
|
||||||
|
|
||||||
if (!_activityWatcher.InGame)
|
|
||||||
{
|
|
||||||
App.Logger.WriteLine(LOG_IDENT, "Not in game, clearing presence");
|
|
||||||
|
|
||||||
_currentPresence = _originalPresence = null;
|
|
||||||
_messageQueue.Clear();
|
|
||||||
|
|
||||||
UpdatePresence();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
string icon = "roblox";
|
|
||||||
string smallImageText = "Roblox";
|
|
||||||
string smallImage = "roblox";
|
|
||||||
|
|
||||||
|
|
||||||
var activity = _activityWatcher.Data;
|
|
||||||
long placeId = activity.PlaceId;
|
|
||||||
|
|
||||||
App.Logger.WriteLine(LOG_IDENT, $"Setting presence for Place ID {placeId}");
|
|
||||||
|
|
||||||
// preserve time spent playing if we're teleporting between places in the same universe
|
|
||||||
var timeStarted = activity.TimeJoined;
|
|
||||||
|
|
||||||
if (activity.RootActivity is not null)
|
|
||||||
timeStarted = activity.RootActivity.TimeJoined;
|
|
||||||
|
|
||||||
if (activity.UniverseDetails is null)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
await UniverseDetails.FetchSingle(activity.UniverseId);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
App.Logger.WriteException(LOG_IDENT, ex);
|
|
||||||
Frontend.ShowMessageBox($"{Strings.ActivityWatcher_RichPresenceLoadFailed}\n\n{ex.Message}", MessageBoxImage.Warning);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
activity.UniverseDetails = UniverseDetails.LoadFromCache(activity.UniverseId);
|
|
||||||
}
|
|
||||||
|
|
||||||
var universeDetails = activity.UniverseDetails!;
|
|
||||||
|
|
||||||
icon = universeDetails.Thumbnail.ImageUrl;
|
|
||||||
|
|
||||||
if (App.Settings.Prop.ShowAccountOnRichPresence)
|
|
||||||
{
|
|
||||||
var userDetails = await UserDetails.Fetch(activity.UserId);
|
|
||||||
|
|
||||||
smallImage = userDetails.Thumbnail.ImageUrl;
|
|
||||||
smallImageText = $"Playing on {userDetails.Data.DisplayName} (@{userDetails.Data.Name})"; // i.e. "axell (@Axelan_se)"
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!_activityWatcher.InGame || placeId != activity.PlaceId)
|
|
||||||
{
|
|
||||||
App.Logger.WriteLine(LOG_IDENT, "Aborting presence set because game activity has changed");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
string status = _activityWatcher.Data.ServerType switch
|
|
||||||
{
|
|
||||||
ServerType.Private => "In a private server",
|
|
||||||
ServerType.Reserved => "In a reserved server",
|
|
||||||
_ => $"by {universeDetails.Data.Creator.Name}" + (universeDetails.Data.Creator.HasVerifiedBadge ? " ☑️" : ""),
|
|
||||||
};
|
|
||||||
|
|
||||||
string universeName = universeDetails.Data.Name;
|
|
||||||
|
|
||||||
if (universeName.Length < 2)
|
|
||||||
universeName = $"{universeName}\x2800\x2800\x2800";
|
|
||||||
|
|
||||||
_currentPresence = new DiscordRPC.RichPresence
|
|
||||||
{
|
|
||||||
Details = universeName,
|
|
||||||
State = status,
|
|
||||||
Timestamps = new Timestamps { Start = timeStarted.ToUniversalTime() },
|
|
||||||
Buttons = GetButtons(),
|
|
||||||
Assets = new Assets
|
|
||||||
{
|
|
||||||
LargeImageKey = icon,
|
|
||||||
LargeImageText = universeDetails.Data.Name,
|
|
||||||
SmallImageKey = smallImage,
|
|
||||||
SmallImageText = smallImageText
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// this is used for configuration from BloxstrapRPC
|
|
||||||
_originalPresence = _currentPresence.Clone();
|
|
||||||
|
|
||||||
if (_messageQueue.Any())
|
|
||||||
{
|
|
||||||
App.Logger.WriteLine(LOG_IDENT, "Processing queued messages");
|
|
||||||
ProcessRPCMessage(_messageQueue.Dequeue(), false);
|
|
||||||
}
|
|
||||||
|
|
||||||
UpdatePresence();
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Button[] GetButtons()
|
|
||||||
{
|
|
||||||
var buttons = new List<Button>();
|
|
||||||
|
|
||||||
var data = _activityWatcher.Data;
|
|
||||||
|
|
||||||
if (!App.Settings.Prop.HideRPCButtons)
|
|
||||||
{
|
|
||||||
bool show = false;
|
|
||||||
|
|
||||||
if (data.ServerType == ServerType.Public)
|
|
||||||
show = true;
|
|
||||||
else if (data.ServerType == ServerType.Reserved && !String.IsNullOrEmpty(data.RPCLaunchData))
|
|
||||||
show = true;
|
|
||||||
|
|
||||||
if (show)
|
|
||||||
{
|
|
||||||
buttons.Add(new Button
|
|
||||||
{
|
|
||||||
Label = "Join server",
|
|
||||||
Url = data.GetInviteDeeplink()
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
buttons.Add(new Button
|
|
||||||
{
|
|
||||||
Label = "See game page",
|
|
||||||
Url = $"https://www.roblox.com/games/{data.PlaceId}"
|
|
||||||
});
|
|
||||||
|
|
||||||
return buttons.ToArray();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void UpdatePresence()
|
|
||||||
{
|
|
||||||
const string LOG_IDENT = "DiscordRichPresence::UpdatePresence";
|
|
||||||
|
|
||||||
if (_currentPresence is null)
|
|
||||||
{
|
|
||||||
App.Logger.WriteLine(LOG_IDENT, $"Presence is empty, clearing");
|
|
||||||
_rpcClient.ClearPresence();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
App.Logger.WriteLine(LOG_IDENT, $"Updating presence");
|
|
||||||
|
|
||||||
if (_visible)
|
|
||||||
_rpcClient.SetPresence(_currentPresence);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Dispose()
|
|
||||||
{
|
|
||||||
App.Logger.WriteLine("DiscordRichPresence::Dispose", "Cleaning up Discord RPC and Presence");
|
|
||||||
_rpcClient.ClearPresence();
|
|
||||||
_rpcClient.Dispose();
|
|
||||||
GC.SuppressFinalize(this);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,93 +0,0 @@
|
|||||||
using System.Runtime.CompilerServices;
|
|
||||||
|
|
||||||
namespace Bloxstrap
|
|
||||||
{
|
|
||||||
public class JsonManager<T> where T : class, new()
|
|
||||||
{
|
|
||||||
public T OriginalProp { get; set; } = new();
|
|
||||||
|
|
||||||
public T Prop { get; set; } = new();
|
|
||||||
|
|
||||||
public virtual string ClassName => typeof(T).Name;
|
|
||||||
|
|
||||||
public virtual string FileLocation => Path.Combine(Paths.Base, $"{ClassName}.json");
|
|
||||||
|
|
||||||
public virtual string LOG_IDENT_CLASS => $"JsonManager<{ClassName}>";
|
|
||||||
|
|
||||||
public virtual void Load(bool alertFailure = true)
|
|
||||||
{
|
|
||||||
string LOG_IDENT = $"{LOG_IDENT_CLASS}::Load";
|
|
||||||
|
|
||||||
App.Logger.WriteLine(LOG_IDENT, $"Loading from {FileLocation}...");
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
T? settings = JsonSerializer.Deserialize<T>(File.ReadAllText(FileLocation));
|
|
||||||
|
|
||||||
if (settings is null)
|
|
||||||
throw new ArgumentNullException("Deserialization returned null");
|
|
||||||
|
|
||||||
Prop = settings;
|
|
||||||
|
|
||||||
App.Logger.WriteLine(LOG_IDENT, "Loaded successfully!");
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
App.Logger.WriteLine(LOG_IDENT, "Failed to load!");
|
|
||||||
App.Logger.WriteException(LOG_IDENT, ex);
|
|
||||||
|
|
||||||
if (alertFailure)
|
|
||||||
{
|
|
||||||
string message = "";
|
|
||||||
|
|
||||||
if (ClassName == nameof(Settings))
|
|
||||||
message = Strings.JsonManager_SettingsLoadFailed;
|
|
||||||
else if (ClassName == nameof(FastFlagManager))
|
|
||||||
message = Strings.JsonManager_FastFlagsLoadFailed;
|
|
||||||
|
|
||||||
if (!String.IsNullOrEmpty(message))
|
|
||||||
Frontend.ShowMessageBox($"{message}\n\n{ex.Message}", System.Windows.MessageBoxImage.Warning);
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
// Create a backup of loaded file
|
|
||||||
File.Copy(FileLocation, FileLocation + ".bak", true);
|
|
||||||
}
|
|
||||||
catch (Exception copyEx)
|
|
||||||
{
|
|
||||||
App.Logger.WriteLine(LOG_IDENT, $"Failed to create backup file: {FileLocation}.bak");
|
|
||||||
App.Logger.WriteException(LOG_IDENT, copyEx);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Save();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public virtual void Save()
|
|
||||||
{
|
|
||||||
string LOG_IDENT = $"{LOG_IDENT_CLASS}::Save";
|
|
||||||
|
|
||||||
App.Logger.WriteLine(LOG_IDENT, $"Saving to {FileLocation}...");
|
|
||||||
|
|
||||||
Directory.CreateDirectory(Path.GetDirectoryName(FileLocation)!);
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
File.WriteAllText(FileLocation, JsonSerializer.Serialize(Prop, new JsonSerializerOptions { WriteIndented = true }));
|
|
||||||
}
|
|
||||||
catch (Exception ex) when (ex is IOException or UnauthorizedAccessException)
|
|
||||||
{
|
|
||||||
App.Logger.WriteLine(LOG_IDENT, "Failed to save");
|
|
||||||
App.Logger.WriteException(LOG_IDENT, ex);
|
|
||||||
|
|
||||||
string errorMessage = string.Format(Resources.Strings.Bootstrapper_JsonManagerSaveFailed, ClassName, ex.Message);
|
|
||||||
Frontend.ShowMessageBox(errorMessage, System.Windows.MessageBoxImage.Warning);
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
App.Logger.WriteLine(LOG_IDENT, "Save complete!");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,299 +0,0 @@
|
|||||||
using System.Windows;
|
|
||||||
|
|
||||||
using Windows.Win32;
|
|
||||||
using Windows.Win32.Foundation;
|
|
||||||
|
|
||||||
using Bloxstrap.UI.Elements.Dialogs;
|
|
||||||
|
|
||||||
namespace Bloxstrap
|
|
||||||
{
|
|
||||||
public static class LaunchHandler
|
|
||||||
{
|
|
||||||
public static void ProcessNextAction(NextAction action, bool isUnfinishedInstall = false)
|
|
||||||
{
|
|
||||||
const string LOG_IDENT = "LaunchHandler::ProcessNextAction";
|
|
||||||
|
|
||||||
switch (action)
|
|
||||||
{
|
|
||||||
case NextAction.LaunchSettings:
|
|
||||||
App.Logger.WriteLine(LOG_IDENT, "Opening settings");
|
|
||||||
LaunchSettings();
|
|
||||||
break;
|
|
||||||
|
|
||||||
case NextAction.LaunchRoblox:
|
|
||||||
App.Logger.WriteLine(LOG_IDENT, "Opening Roblox");
|
|
||||||
LaunchRoblox(LaunchMode.Player);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case NextAction.LaunchRobloxStudio:
|
|
||||||
App.Logger.WriteLine(LOG_IDENT, "Opening Roblox Studio");
|
|
||||||
LaunchRoblox(LaunchMode.Studio);
|
|
||||||
break;
|
|
||||||
|
|
||||||
default:
|
|
||||||
App.Logger.WriteLine(LOG_IDENT, "Closing");
|
|
||||||
App.Terminate(isUnfinishedInstall ? ErrorCode.ERROR_INSTALL_USEREXIT : ErrorCode.ERROR_SUCCESS);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void ProcessLaunchArgs()
|
|
||||||
{
|
|
||||||
const string LOG_IDENT = "LaunchHandler::ProcessLaunchArgs";
|
|
||||||
|
|
||||||
// this order is specific
|
|
||||||
|
|
||||||
if (App.LaunchSettings.UninstallFlag.Active)
|
|
||||||
{
|
|
||||||
App.Logger.WriteLine(LOG_IDENT, "Opening uninstaller");
|
|
||||||
LaunchUninstaller();
|
|
||||||
}
|
|
||||||
else if (App.LaunchSettings.MenuFlag.Active)
|
|
||||||
{
|
|
||||||
App.Logger.WriteLine(LOG_IDENT, "Opening settings");
|
|
||||||
LaunchSettings();
|
|
||||||
}
|
|
||||||
else if (App.LaunchSettings.WatcherFlag.Active)
|
|
||||||
{
|
|
||||||
App.Logger.WriteLine(LOG_IDENT, "Opening watcher");
|
|
||||||
LaunchWatcher();
|
|
||||||
}
|
|
||||||
else if (App.LaunchSettings.RobloxLaunchMode != LaunchMode.None)
|
|
||||||
{
|
|
||||||
App.Logger.WriteLine(LOG_IDENT, $"Opening bootstrapper ({App.LaunchSettings.RobloxLaunchMode})");
|
|
||||||
LaunchRoblox(App.LaunchSettings.RobloxLaunchMode);
|
|
||||||
}
|
|
||||||
else if (!App.LaunchSettings.QuietFlag.Active)
|
|
||||||
{
|
|
||||||
App.Logger.WriteLine(LOG_IDENT, "Opening menu");
|
|
||||||
LaunchMenu();
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
App.Logger.WriteLine(LOG_IDENT, "Closing - quiet flag active");
|
|
||||||
App.Terminate();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void LaunchInstaller()
|
|
||||||
{
|
|
||||||
using var interlock = new InterProcessLock("Installer");
|
|
||||||
|
|
||||||
if (!interlock.IsAcquired)
|
|
||||||
{
|
|
||||||
Frontend.ShowMessageBox(Strings.Dialog_AlreadyRunning_Installer, MessageBoxImage.Stop);
|
|
||||||
App.Terminate();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (App.LaunchSettings.UninstallFlag.Active)
|
|
||||||
{
|
|
||||||
Frontend.ShowMessageBox(Strings.Bootstrapper_FirstRunUninstall, MessageBoxImage.Error);
|
|
||||||
App.Terminate(ErrorCode.ERROR_INVALID_FUNCTION);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (App.LaunchSettings.QuietFlag.Active)
|
|
||||||
{
|
|
||||||
var installer = new Installer();
|
|
||||||
|
|
||||||
if (!installer.CheckInstallLocation())
|
|
||||||
App.Terminate(ErrorCode.ERROR_INSTALL_FAILURE);
|
|
||||||
|
|
||||||
installer.DoInstall();
|
|
||||||
|
|
||||||
interlock.Dispose();
|
|
||||||
|
|
||||||
ProcessLaunchArgs();
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
#if QA_BUILD
|
|
||||||
Frontend.ShowMessageBox("You are about to install a QA build of Bloxstrap. The red window border indicates that this is a QA build.\n\nQA builds are handled completely separately of your standard installation, like a virtual environment.", MessageBoxImage.Information);
|
|
||||||
#endif
|
|
||||||
|
|
||||||
new LanguageSelectorDialog().ShowDialog();
|
|
||||||
|
|
||||||
var installer = new UI.Elements.Installer.MainWindow();
|
|
||||||
installer.ShowDialog();
|
|
||||||
|
|
||||||
interlock.Dispose();
|
|
||||||
|
|
||||||
ProcessNextAction(installer.CloseAction, !installer.Finished);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void LaunchUninstaller()
|
|
||||||
{
|
|
||||||
using var interlock = new InterProcessLock("Uninstaller");
|
|
||||||
|
|
||||||
if (!interlock.IsAcquired)
|
|
||||||
{
|
|
||||||
Frontend.ShowMessageBox(Strings.Dialog_AlreadyRunning_Uninstaller, MessageBoxImage.Stop);
|
|
||||||
App.Terminate();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool confirmed = false;
|
|
||||||
bool keepData = true;
|
|
||||||
|
|
||||||
if (App.LaunchSettings.QuietFlag.Active)
|
|
||||||
{
|
|
||||||
confirmed = true;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
var dialog = new UninstallerDialog();
|
|
||||||
dialog.ShowDialog();
|
|
||||||
|
|
||||||
confirmed = dialog.Confirmed;
|
|
||||||
keepData = dialog.KeepData;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!confirmed)
|
|
||||||
{
|
|
||||||
App.Terminate();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
Installer.DoUninstall(keepData);
|
|
||||||
|
|
||||||
Frontend.ShowMessageBox(Strings.Bootstrapper_SuccessfullyUninstalled, MessageBoxImage.Information);
|
|
||||||
|
|
||||||
App.Terminate();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void LaunchSettings()
|
|
||||||
{
|
|
||||||
const string LOG_IDENT = "LaunchHandler::LaunchSettings";
|
|
||||||
|
|
||||||
using var interlock = new InterProcessLock("Settings");
|
|
||||||
|
|
||||||
if (interlock.IsAcquired)
|
|
||||||
{
|
|
||||||
bool showAlreadyRunningWarning = Process.GetProcessesByName(App.ProjectName).Length > 1;
|
|
||||||
|
|
||||||
var window = new UI.Elements.Settings.MainWindow(showAlreadyRunningWarning);
|
|
||||||
|
|
||||||
// typically we'd use Show(), but we need to block to ensure IPL stays in scope
|
|
||||||
window.ShowDialog();
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
App.Logger.WriteLine(LOG_IDENT, "Found an already existing menu window");
|
|
||||||
|
|
||||||
var process = Utilities.GetProcessesSafe().Where(x => x.MainWindowTitle == Strings.Menu_Title).FirstOrDefault();
|
|
||||||
|
|
||||||
if (process is not null)
|
|
||||||
PInvoke.SetForegroundWindow((HWND)process.MainWindowHandle);
|
|
||||||
|
|
||||||
App.Terminate();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void LaunchMenu()
|
|
||||||
{
|
|
||||||
var dialog = new LaunchMenuDialog();
|
|
||||||
dialog.ShowDialog();
|
|
||||||
|
|
||||||
ProcessNextAction(dialog.CloseAction);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void LaunchRoblox(LaunchMode launchMode)
|
|
||||||
{
|
|
||||||
const string LOG_IDENT = "LaunchHandler::LaunchRoblox";
|
|
||||||
|
|
||||||
if (launchMode == LaunchMode.None)
|
|
||||||
throw new InvalidOperationException("No Roblox launch mode set");
|
|
||||||
|
|
||||||
if (!File.Exists(Path.Combine(Paths.System, "mfplat.dll")))
|
|
||||||
{
|
|
||||||
Frontend.ShowMessageBox(Strings.Bootstrapper_WMFNotFound, MessageBoxImage.Error);
|
|
||||||
|
|
||||||
if (!App.LaunchSettings.QuietFlag.Active)
|
|
||||||
Utilities.ShellExecute("https://support.microsoft.com/en-us/topic/media-feature-pack-list-for-windows-n-editions-c1c6fffa-d052-8338-7a79-a4bb980a700a");
|
|
||||||
|
|
||||||
App.Terminate(ErrorCode.ERROR_FILE_NOT_FOUND);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (App.Settings.Prop.ConfirmLaunches && Mutex.TryOpenExisting("ROBLOX_singletonMutex", out var _))
|
|
||||||
{
|
|
||||||
// this currently doesn't work very well since it relies on checking the existence of the singleton mutex
|
|
||||||
// which often hangs around for a few seconds after the window closes
|
|
||||||
// it would be better to have this rely on the activity tracker when we implement IPC in the planned refactoring
|
|
||||||
|
|
||||||
var result = Frontend.ShowMessageBox(Strings.Bootstrapper_ConfirmLaunch, MessageBoxImage.Warning, MessageBoxButton.YesNo);
|
|
||||||
|
|
||||||
if (result != MessageBoxResult.Yes)
|
|
||||||
{
|
|
||||||
App.Terminate();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// start bootstrapper and show the bootstrapper modal if we're not running silently
|
|
||||||
App.Logger.WriteLine(LOG_IDENT, "Initializing bootstrapper");
|
|
||||||
App.Bootstrapper = new Bootstrapper(launchMode);
|
|
||||||
IBootstrapperDialog? dialog = null;
|
|
||||||
|
|
||||||
if (!App.LaunchSettings.QuietFlag.Active)
|
|
||||||
{
|
|
||||||
App.Logger.WriteLine(LOG_IDENT, "Initializing bootstrapper dialog");
|
|
||||||
dialog = App.Settings.Prop.BootstrapperStyle.GetNew();
|
|
||||||
App.Bootstrapper.Dialog = dialog;
|
|
||||||
dialog.Bootstrapper = App.Bootstrapper;
|
|
||||||
}
|
|
||||||
|
|
||||||
Task.Run(App.Bootstrapper.Run).ContinueWith(t =>
|
|
||||||
{
|
|
||||||
App.Logger.WriteLine(LOG_IDENT, "Bootstrapper task has finished");
|
|
||||||
|
|
||||||
if (t.IsFaulted)
|
|
||||||
{
|
|
||||||
App.Logger.WriteLine(LOG_IDENT, "An exception occurred when running the bootstrapper");
|
|
||||||
|
|
||||||
if (t.Exception is not null)
|
|
||||||
App.FinalizeExceptionHandling(t.Exception);
|
|
||||||
}
|
|
||||||
|
|
||||||
App.Terminate();
|
|
||||||
});
|
|
||||||
|
|
||||||
dialog?.ShowBootstrapper();
|
|
||||||
|
|
||||||
App.Logger.WriteLine(LOG_IDENT, "Exiting");
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void LaunchWatcher()
|
|
||||||
{
|
|
||||||
const string LOG_IDENT = "LaunchHandler::LaunchWatcher";
|
|
||||||
|
|
||||||
// this whole topology is a bit confusing, bear with me:
|
|
||||||
// main thread: strictly UI only, handles showing of the notification area icon, context menu, server details dialog
|
|
||||||
// - server information task: queries server location, invoked if either the explorer notification is shown or the server details dialog is opened
|
|
||||||
// - discord rpc thread: handles rpc connection with discord
|
|
||||||
// - discord rich presence tasks: handles querying and displaying of game information, invoked on activity watcher events
|
|
||||||
// - watcher task: runs activity watcher + waiting for roblox to close, terminates when it has
|
|
||||||
|
|
||||||
var watcher = new Watcher();
|
|
||||||
|
|
||||||
Task.Run(watcher.Run).ContinueWith(t =>
|
|
||||||
{
|
|
||||||
App.Logger.WriteLine(LOG_IDENT, "Watcher task has finished");
|
|
||||||
|
|
||||||
watcher.Dispose();
|
|
||||||
|
|
||||||
if (t.IsFaulted)
|
|
||||||
{
|
|
||||||
App.Logger.WriteLine(LOG_IDENT, "An exception occurred when running the watcher");
|
|
||||||
|
|
||||||
if (t.Exception is not null)
|
|
||||||
App.FinalizeExceptionHandling(t.Exception);
|
|
||||||
}
|
|
||||||
|
|
||||||
App.Terminate();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,179 +0,0 @@
|
|||||||
using Bloxstrap.Enums;
|
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Reflection;
|
|
||||||
using System.Text;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using System.Web;
|
|
||||||
using System.Windows;
|
|
||||||
|
|
||||||
namespace Bloxstrap
|
|
||||||
{
|
|
||||||
public class LaunchSettings
|
|
||||||
{
|
|
||||||
public LaunchFlag MenuFlag { get; } = new("preferences,menu,settings");
|
|
||||||
|
|
||||||
public LaunchFlag WatcherFlag { get; } = new("watcher");
|
|
||||||
|
|
||||||
public LaunchFlag QuietFlag { get; } = new("quiet");
|
|
||||||
|
|
||||||
public LaunchFlag UninstallFlag { get; } = new("uninstall");
|
|
||||||
|
|
||||||
public LaunchFlag NoLaunchFlag { get; } = new("nolaunch");
|
|
||||||
|
|
||||||
public LaunchFlag TestModeFlag { get; } = new("testmode");
|
|
||||||
|
|
||||||
public LaunchFlag NoGPUFlag { get; } = new("nogpu");
|
|
||||||
|
|
||||||
public LaunchFlag UpgradeFlag { get; } = new("upgrade");
|
|
||||||
|
|
||||||
public LaunchFlag PlayerFlag { get; } = new("player");
|
|
||||||
|
|
||||||
public LaunchFlag StudioFlag { get; } = new("studio");
|
|
||||||
|
|
||||||
#if DEBUG
|
|
||||||
public bool BypassUpdateCheck => true;
|
|
||||||
#else
|
|
||||||
public bool BypassUpdateCheck => UninstallFlag.Active || WatcherFlag.Active;
|
|
||||||
#endif
|
|
||||||
|
|
||||||
public LaunchMode RobloxLaunchMode { get; set; } = LaunchMode.None;
|
|
||||||
|
|
||||||
public string RobloxLaunchArgs { get; set; } = "";
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Original launch arguments
|
|
||||||
/// </summary>
|
|
||||||
public string[] Args { get; private set; }
|
|
||||||
|
|
||||||
private readonly Dictionary<string, LaunchFlag> _flagMap = new();
|
|
||||||
|
|
||||||
public LaunchSettings(string[] args)
|
|
||||||
{
|
|
||||||
const string LOG_IDENT = "LaunchSettings";
|
|
||||||
|
|
||||||
#if DEBUG
|
|
||||||
App.Logger.WriteLine(LOG_IDENT, $"Launched with arguments: {string.Join(' ', args)}");
|
|
||||||
#endif
|
|
||||||
|
|
||||||
Args = args;
|
|
||||||
|
|
||||||
// build flag map
|
|
||||||
foreach (var prop in this.GetType().GetProperties())
|
|
||||||
{
|
|
||||||
if (prop.PropertyType != typeof(LaunchFlag))
|
|
||||||
continue;
|
|
||||||
|
|
||||||
if (prop.GetValue(this) is not LaunchFlag flag)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
foreach (string identifier in flag.Identifiers.Split(','))
|
|
||||||
_flagMap.Add(identifier, flag);
|
|
||||||
}
|
|
||||||
|
|
||||||
int startIdx = 0;
|
|
||||||
|
|
||||||
// infer roblox launch uris
|
|
||||||
if (Args.Length >= 1)
|
|
||||||
{
|
|
||||||
string arg = Args[0];
|
|
||||||
|
|
||||||
if (arg.StartsWith("roblox:", StringComparison.OrdinalIgnoreCase)
|
|
||||||
|| arg.StartsWith("roblox-player:", StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
App.Logger.WriteLine(LOG_IDENT, "Got Roblox player argument");
|
|
||||||
RobloxLaunchMode = LaunchMode.Player;
|
|
||||||
RobloxLaunchArgs = arg;
|
|
||||||
startIdx = 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// parse
|
|
||||||
for (int i = startIdx; i < Args.Length; i++)
|
|
||||||
{
|
|
||||||
string arg = Args[i];
|
|
||||||
|
|
||||||
if (!arg.StartsWith('-'))
|
|
||||||
{
|
|
||||||
App.Logger.WriteLine(LOG_IDENT, $"Invalid argument: {arg}");
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
string identifier = arg[1..];
|
|
||||||
|
|
||||||
if (!_flagMap.TryGetValue(identifier, out LaunchFlag? flag) || flag is null)
|
|
||||||
{
|
|
||||||
App.Logger.WriteLine(LOG_IDENT, $"Unknown argument: {identifier}");
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
flag.Active = true;
|
|
||||||
|
|
||||||
if (i < Args.Length - 1 && Args[i+1] is string nextArg && !nextArg.StartsWith('-'))
|
|
||||||
{
|
|
||||||
flag.Data = nextArg;
|
|
||||||
i++;
|
|
||||||
App.Logger.WriteLine(LOG_IDENT, $"Identifier '{identifier}' is active with data");
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
App.Logger.WriteLine(LOG_IDENT, $"Identifier '{identifier}' is active");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (PlayerFlag.Active)
|
|
||||||
ParsePlayer(PlayerFlag.Data);
|
|
||||||
else if (StudioFlag.Active)
|
|
||||||
ParseStudio(StudioFlag.Data);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void ParsePlayer(string? data)
|
|
||||||
{
|
|
||||||
const string LOG_IDENT = "LaunchSettings::ParsePlayer";
|
|
||||||
|
|
||||||
RobloxLaunchMode = LaunchMode.Player;
|
|
||||||
|
|
||||||
if (!String.IsNullOrEmpty(data))
|
|
||||||
{
|
|
||||||
App.Logger.WriteLine(LOG_IDENT, "Got Roblox launch arguments");
|
|
||||||
RobloxLaunchArgs = data;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
App.Logger.WriteLine(LOG_IDENT, "No Roblox launch arguments were provided");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void ParseStudio(string? data)
|
|
||||||
{
|
|
||||||
const string LOG_IDENT = "LaunchSettings::ParseStudio";
|
|
||||||
|
|
||||||
RobloxLaunchMode = LaunchMode.Studio;
|
|
||||||
|
|
||||||
if (String.IsNullOrEmpty(data))
|
|
||||||
{
|
|
||||||
App.Logger.WriteLine(LOG_IDENT, "No Roblox launch arguments were provided");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (data.StartsWith("roblox-studio:"))
|
|
||||||
{
|
|
||||||
App.Logger.WriteLine(LOG_IDENT, "Got Roblox Studio launch arguments");
|
|
||||||
RobloxLaunchArgs = data;
|
|
||||||
}
|
|
||||||
else if (data.StartsWith("roblox-studio-auth:"))
|
|
||||||
{
|
|
||||||
App.Logger.WriteLine(LOG_IDENT, "Got Roblox Studio Auth launch arguments");
|
|
||||||
RobloxLaunchMode = LaunchMode.StudioAuth;
|
|
||||||
RobloxLaunchArgs = data;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
// likely a local path
|
|
||||||
App.Logger.WriteLine(LOG_IDENT, "Got Roblox Studio local place file");
|
|
||||||
RobloxLaunchArgs = $"-task EditFile -localPlaceFile \"{data}\"";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,133 +0,0 @@
|
|||||||
using System.Windows;
|
|
||||||
|
|
||||||
namespace Bloxstrap
|
|
||||||
{
|
|
||||||
internal static class Locale
|
|
||||||
{
|
|
||||||
public static CultureInfo CurrentCulture { get; private set; } = CultureInfo.InvariantCulture;
|
|
||||||
|
|
||||||
public static bool RightToLeft { get; private set; } = false;
|
|
||||||
|
|
||||||
private static readonly List<string> _rtlLocales = new() { "ar", "he", "fa" };
|
|
||||||
|
|
||||||
public static readonly Dictionary<string, string> SupportedLocales = new()
|
|
||||||
{
|
|
||||||
{ "nil", Strings.Common_SystemDefault },
|
|
||||||
{ "en", "English" },
|
|
||||||
{ "en-US", "English (United States)" },
|
|
||||||
#if QA_BUILD
|
|
||||||
{ "sq", "Albanian" }, // Albanian (TODO: translate string)
|
|
||||||
#endif
|
|
||||||
{ "ar", "العربية" }, // Arabic
|
|
||||||
{ "bg", "Български" }, // Bulgarian
|
|
||||||
#if QA_BUILD
|
|
||||||
{ "bn", "বাংলা" }, // Bengali
|
|
||||||
{ "bs", "Bosanski" }, // Bosnian
|
|
||||||
#endif
|
|
||||||
{ "cs", "Čeština" }, // Czech
|
|
||||||
{ "de", "Deutsch" }, // German
|
|
||||||
#if QA_BUILD
|
|
||||||
{ "da", "Dansk" }, // Danish
|
|
||||||
#endif
|
|
||||||
{ "es-ES", "Español" }, // Spanish
|
|
||||||
#if QA_BUILD
|
|
||||||
{ "el", "Ελληνικά" }, // Greek
|
|
||||||
#endif
|
|
||||||
{ "fa", "فارسی" }, // Persian
|
|
||||||
{ "fi", "Suomi" }, // Finnish
|
|
||||||
{ "fil", "Filipino" }, // Filipino
|
|
||||||
{ "fr", "Français" }, // French
|
|
||||||
#if QA_BUILD
|
|
||||||
{ "he", "עברית" }, // Hebrew
|
|
||||||
{ "hi", "Hindi (Latin)" }, // Hindi
|
|
||||||
#endif
|
|
||||||
{ "hr", "Hrvatski" }, // Croatian
|
|
||||||
{ "hu", "Magyar" }, // Hungarian
|
|
||||||
{ "id", "Bahasa Indonesia" }, // Indonesian
|
|
||||||
{ "it", "Italiano" }, // Italian
|
|
||||||
{ "ja", "日本語" }, // Japanese
|
|
||||||
{ "ko", "한국어" }, // Korean
|
|
||||||
{ "lt", "Lietuvių" }, // Lithuanian
|
|
||||||
{ "ms", "Malay" }, // Malay
|
|
||||||
{ "nl", "Nederlands" }, // Dutch
|
|
||||||
#if QA_BUILD
|
|
||||||
{ "no", "Bokmål" }, // Norwegian
|
|
||||||
#endif
|
|
||||||
{ "pl", "Polski" }, // Polish
|
|
||||||
{ "pt-BR", "Português (Brasil)" }, // Portuguese, Brazilian
|
|
||||||
{ "ro", "Română" }, // Romanian
|
|
||||||
{ "ru", "Русский" }, // Russian
|
|
||||||
{ "sv-SE", "Svenska" }, // Swedish
|
|
||||||
{ "th", "ภาษาไทย" }, // Thai
|
|
||||||
{ "tr", "Türkçe" }, // Turkish
|
|
||||||
{ "uk", "Українська" }, // Ukrainian
|
|
||||||
{ "vi", "Tiếng Việt" }, // Vietnamese
|
|
||||||
{ "zh-CN", "中文 (简体)" }, // Chinese Simplified
|
|
||||||
#if QA_BUILD
|
|
||||||
{ "zh-HK", "中文 (廣東話)" }, // Chinese Traditional, Hong Kong
|
|
||||||
#endif
|
|
||||||
{ "zh-TW", "中文 (繁體)" } // Chinese Traditional
|
|
||||||
};
|
|
||||||
|
|
||||||
public static string GetIdentifierFromName(string language) => SupportedLocales.FirstOrDefault(x => x.Value == language).Key ?? "nil";
|
|
||||||
|
|
||||||
public static List<string> GetLanguages()
|
|
||||||
{
|
|
||||||
var languages = new List<string>();
|
|
||||||
|
|
||||||
languages.AddRange(SupportedLocales.Values.Take(3));
|
|
||||||
languages.AddRange(SupportedLocales.Values.Where(x => !languages.Contains(x)).OrderBy(x => x));
|
|
||||||
languages[0] = Strings.Common_SystemDefault; // set again for any locale changes
|
|
||||||
|
|
||||||
return languages;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void Set(string identifier)
|
|
||||||
{
|
|
||||||
if (!SupportedLocales.ContainsKey(identifier))
|
|
||||||
identifier = "nil";
|
|
||||||
|
|
||||||
if (identifier == "nil")
|
|
||||||
{
|
|
||||||
CurrentCulture = Thread.CurrentThread.CurrentUICulture;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
CurrentCulture = new CultureInfo(identifier);
|
|
||||||
|
|
||||||
CultureInfo.DefaultThreadCurrentUICulture = CurrentCulture;
|
|
||||||
Thread.CurrentThread.CurrentUICulture = CurrentCulture;
|
|
||||||
}
|
|
||||||
|
|
||||||
RightToLeft = _rtlLocales.Any(CurrentCulture.Name.StartsWith);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void Initialize()
|
|
||||||
{
|
|
||||||
Set("nil");
|
|
||||||
|
|
||||||
// https://supportcenter.devexpress.com/ticket/details/t905790/is-there-a-way-to-set-right-to-left-mode-in-wpf-for-the-whole-application
|
|
||||||
EventManager.RegisterClassHandler(typeof(Window), FrameworkElement.LoadedEvent, new RoutedEventHandler((sender, _) =>
|
|
||||||
{
|
|
||||||
var window = (Window)sender;
|
|
||||||
|
|
||||||
if (RightToLeft)
|
|
||||||
{
|
|
||||||
window.FlowDirection = FlowDirection.RightToLeft;
|
|
||||||
|
|
||||||
if (window.ContextMenu is not null)
|
|
||||||
window.ContextMenu.FlowDirection = FlowDirection.RightToLeft;
|
|
||||||
}
|
|
||||||
else if (CurrentCulture.Name.StartsWith("th"))
|
|
||||||
{
|
|
||||||
window.FontFamily = new System.Windows.Media.FontFamily(new Uri("pack://application:,,,/Resources/Fonts/"), "./#Noto Sans Thai");
|
|
||||||
}
|
|
||||||
|
|
||||||
#if QA_BUILD
|
|
||||||
window.BorderBrush = System.Windows.Media.Brushes.Red;
|
|
||||||
window.BorderThickness = new Thickness(4);
|
|
||||||
#endif
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,145 +0,0 @@
|
|||||||
namespace Bloxstrap
|
|
||||||
{
|
|
||||||
// https://stackoverflow.com/a/53873141/11852173
|
|
||||||
|
|
||||||
public class Logger
|
|
||||||
{
|
|
||||||
private readonly SemaphoreSlim _semaphore = new(1, 1);
|
|
||||||
private FileStream? _filestream;
|
|
||||||
|
|
||||||
public readonly List<string> History = new();
|
|
||||||
public bool Initialized = false;
|
|
||||||
public bool NoWriteMode = false;
|
|
||||||
public string? FileLocation;
|
|
||||||
|
|
||||||
public string AsDocument => String.Join('\n', History);
|
|
||||||
|
|
||||||
public void Initialize(bool useTempDir = false)
|
|
||||||
{
|
|
||||||
const string LOG_IDENT = "Logger::Initialize";
|
|
||||||
|
|
||||||
string directory = useTempDir ? Path.Combine(Paths.TempLogs) : Path.Combine(Paths.Base, "Logs");
|
|
||||||
string timestamp = DateTime.UtcNow.ToString("yyyyMMdd'T'HHmmss'Z'");
|
|
||||||
string filename = $"{App.ProjectName}_{timestamp}.log";
|
|
||||||
string location = Path.Combine(directory, filename);
|
|
||||||
|
|
||||||
WriteLine(LOG_IDENT, $"Initializing at {location}");
|
|
||||||
|
|
||||||
if (Initialized)
|
|
||||||
{
|
|
||||||
WriteLine(LOG_IDENT, "Failed to initialize because logger is already initialized");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
Directory.CreateDirectory(directory);
|
|
||||||
|
|
||||||
if (File.Exists(location))
|
|
||||||
{
|
|
||||||
WriteLine(LOG_IDENT, "Failed to initialize because log file already exists");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
_filestream = File.Open(location, FileMode.Create, FileAccess.Write, FileShare.Read);
|
|
||||||
}
|
|
||||||
catch (IOException)
|
|
||||||
{
|
|
||||||
WriteLine(LOG_IDENT, "Failed to initialize because log file already exists");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
catch (UnauthorizedAccessException)
|
|
||||||
{
|
|
||||||
if (NoWriteMode)
|
|
||||||
return;
|
|
||||||
|
|
||||||
WriteLine(LOG_IDENT, $"Failed to initialize because Bloxstrap cannot write to {directory}");
|
|
||||||
|
|
||||||
Frontend.ShowMessageBox(
|
|
||||||
String.Format(Strings.Logger_NoWriteMode, directory),
|
|
||||||
System.Windows.MessageBoxImage.Warning,
|
|
||||||
System.Windows.MessageBoxButton.OK
|
|
||||||
);
|
|
||||||
|
|
||||||
NoWriteMode = true;
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
Initialized = true;
|
|
||||||
|
|
||||||
if (History.Count > 0)
|
|
||||||
WriteToLog(string.Join("\r\n", History));
|
|
||||||
|
|
||||||
WriteLine(LOG_IDENT, "Finished initializing!");
|
|
||||||
|
|
||||||
FileLocation = location;
|
|
||||||
|
|
||||||
// clean up any logs older than a week
|
|
||||||
if (Paths.Initialized && Directory.Exists(Paths.Logs))
|
|
||||||
{
|
|
||||||
foreach (FileInfo log in new DirectoryInfo(Paths.Logs).GetFiles())
|
|
||||||
{
|
|
||||||
if (log.LastWriteTimeUtc.AddDays(7) > DateTime.UtcNow)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
WriteLine(LOG_IDENT, $"Cleaning up old log file '{log.Name}'");
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
log.Delete();
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
WriteLine(LOG_IDENT, "Failed to delete log!");
|
|
||||||
WriteException(LOG_IDENT, ex);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void WriteLine(string message)
|
|
||||||
{
|
|
||||||
string timestamp = DateTime.UtcNow.ToString("s") + "Z";
|
|
||||||
string outcon = $"{timestamp} {message}";
|
|
||||||
string outlog = outcon.Replace(Paths.UserProfile, "%UserProfile%", StringComparison.InvariantCultureIgnoreCase);
|
|
||||||
|
|
||||||
Debug.WriteLine(outcon);
|
|
||||||
WriteToLog(outlog);
|
|
||||||
|
|
||||||
History.Add(outlog);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void WriteLine(string identifier, string message) => WriteLine($"[{identifier}] {message}");
|
|
||||||
|
|
||||||
public void WriteException(string identifier, Exception ex)
|
|
||||||
{
|
|
||||||
Thread.CurrentThread.CurrentUICulture = CultureInfo.InvariantCulture;
|
|
||||||
|
|
||||||
string hresult = "0x" + ex.HResult.ToString("X8");
|
|
||||||
|
|
||||||
WriteLine($"[{identifier}] ({hresult}) {ex}");
|
|
||||||
|
|
||||||
Thread.CurrentThread.CurrentUICulture = Locale.CurrentCulture;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async void WriteToLog(string message)
|
|
||||||
{
|
|
||||||
if (!Initialized)
|
|
||||||
return;
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
await _semaphore.WaitAsync();
|
|
||||||
await _filestream!.WriteAsync(Encoding.UTF8.GetBytes($"{message}\r\n"));
|
|
||||||
|
|
||||||
_ = _filestream.FlushAsync();
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
_semaphore.Release();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,13 +0,0 @@
|
|||||||
namespace Bloxstrap.Models.APIs.Config
|
|
||||||
{
|
|
||||||
public class Supporter
|
|
||||||
{
|
|
||||||
[JsonPropertyName("imageAsset")]
|
|
||||||
public string ImageAsset { get; set; } = null!;
|
|
||||||
|
|
||||||
[JsonPropertyName("name")]
|
|
||||||
public string Name { get; set; } = null!;
|
|
||||||
|
|
||||||
public string Image => $"https://raw.githubusercontent.com/bloxstraplabs/config/main/assets/{ImageAsset}";
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,11 +0,0 @@
|
|||||||
namespace Bloxstrap.Models.APIs.Config
|
|
||||||
{
|
|
||||||
public class SupporterData
|
|
||||||
{
|
|
||||||
[JsonPropertyName("monthly")]
|
|
||||||
public SupporterGroup Monthly { get; set; } = new();
|
|
||||||
|
|
||||||
[JsonPropertyName("oneoff")]
|
|
||||||
public SupporterGroup OneOff { get; set; } = new();
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,11 +0,0 @@
|
|||||||
namespace Bloxstrap.Models.APIs.Config
|
|
||||||
{
|
|
||||||
public class SupporterGroup
|
|
||||||
{
|
|
||||||
[JsonPropertyName("columns")]
|
|
||||||
public int Columns { get; set; } = 0;
|
|
||||||
|
|
||||||
[JsonPropertyName("supporters")]
|
|
||||||
public List<Supporter> Supporters { get; set; } = Enumerable.Empty<Supporter>().ToList();
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,8 +0,0 @@
|
|||||||
public class GithubReleaseAsset
|
|
||||||
{
|
|
||||||
[JsonPropertyName("browser_download_url")]
|
|
||||||
public string BrowserDownloadUrl { get; set; } = null!;
|
|
||||||
|
|
||||||
[JsonPropertyName("name")]
|
|
||||||
public string Name { get; set; } = null!;
|
|
||||||
}
|
|
@ -1,20 +0,0 @@
|
|||||||
namespace Bloxstrap.Models.APIs.GitHub
|
|
||||||
{
|
|
||||||
public class GithubRelease
|
|
||||||
{
|
|
||||||
[JsonPropertyName("tag_name")]
|
|
||||||
public string TagName { get; set; } = null!;
|
|
||||||
|
|
||||||
[JsonPropertyName("name")]
|
|
||||||
public string Name { get; set; } = null!;
|
|
||||||
|
|
||||||
[JsonPropertyName("body")]
|
|
||||||
public string Body { get; set; } = null!;
|
|
||||||
|
|
||||||
[JsonPropertyName("created_at")]
|
|
||||||
public string CreatedAt { get; set; } = null!;
|
|
||||||
|
|
||||||
[JsonPropertyName("assets")]
|
|
||||||
public List<GithubReleaseAsset>? Assets { get; set; }
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,14 +0,0 @@
|
|||||||
namespace Bloxstrap.Models.APIs
|
|
||||||
{
|
|
||||||
public class IPInfoResponse
|
|
||||||
{
|
|
||||||
[JsonPropertyName("city")]
|
|
||||||
public string City { get; set; } = null!;
|
|
||||||
|
|
||||||
[JsonPropertyName("country")]
|
|
||||||
public string Country { get; set; } = null!;
|
|
||||||
|
|
||||||
[JsonPropertyName("region")]
|
|
||||||
public string Region { get; set; } = null!;
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,11 +0,0 @@
|
|||||||
namespace Bloxstrap.Models.APIs.Roblox
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Roblox.Web.WebAPI.Models.ApiArrayResponse
|
|
||||||
/// </summary>
|
|
||||||
public class ApiArrayResponse<T>
|
|
||||||
{
|
|
||||||
[JsonPropertyName("data")]
|
|
||||||
public IEnumerable<T> Data { get; set; } = null!;
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,8 +0,0 @@
|
|||||||
namespace Bloxstrap.Models.APIs.Roblox
|
|
||||||
{
|
|
||||||
public class ClientFlagSettings
|
|
||||||
{
|
|
||||||
[JsonPropertyName("applicationSettings")]
|
|
||||||
public Dictionary<string, string>? ApplicationSettings { get; set; }
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,16 +0,0 @@
|
|||||||
namespace Bloxstrap.Models.APIs.Roblox
|
|
||||||
{
|
|
||||||
public class ClientVersion
|
|
||||||
{
|
|
||||||
[JsonPropertyName("version")]
|
|
||||||
public string Version { get; set; } = null!;
|
|
||||||
|
|
||||||
[JsonPropertyName("clientVersionUpload")]
|
|
||||||
public string VersionGuid { get; set; } = null!;
|
|
||||||
|
|
||||||
[JsonPropertyName("bootstrapperVersion")]
|
|
||||||
public string BootstrapperVersion { get; set; } = null!;
|
|
||||||
|
|
||||||
public DateTime? Timestamp { get; set; }
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,39 +0,0 @@
|
|||||||
namespace Bloxstrap.Models.APIs.Roblox
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Roblox.Games.Api.Models.Response.GameCreator
|
|
||||||
/// Response model for getting the game creator
|
|
||||||
/// </summary>
|
|
||||||
public class GameCreator
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// The game creator id
|
|
||||||
/// </summary>
|
|
||||||
[JsonPropertyName("id")]
|
|
||||||
public long Id { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The game creator name
|
|
||||||
/// </summary>
|
|
||||||
[JsonPropertyName("name")]
|
|
||||||
public string Name { get; set; } = null!;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The game creator type
|
|
||||||
/// </summary>
|
|
||||||
[JsonPropertyName("type")]
|
|
||||||
public string Type { get; set; } = null!;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The game creator account is Luobu Real Name Verified
|
|
||||||
/// </summary>
|
|
||||||
[JsonPropertyName("isRNVAccount")]
|
|
||||||
public bool IsRNVAccount { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Builder verified badge status.
|
|
||||||
/// </summary>
|
|
||||||
[JsonPropertyName("hasVerifiedBadge")]
|
|
||||||
public bool HasVerifiedBadge { get; set; }
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,151 +0,0 @@
|
|||||||
namespace Bloxstrap.Models.APIs.Roblox
|
|
||||||
{
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Roblox.Games.Api.Models.Response.GameDetailResponse
|
|
||||||
/// Response model for getting the game detail
|
|
||||||
/// </summary>
|
|
||||||
public class GameDetailResponse
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// The game universe id
|
|
||||||
/// </summary>
|
|
||||||
[JsonPropertyName("id")]
|
|
||||||
public long Id { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The game root place id
|
|
||||||
/// </summary>
|
|
||||||
[JsonPropertyName("rootPlaceId")]
|
|
||||||
public long RootPlaceId { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The game name
|
|
||||||
/// </summary>
|
|
||||||
[JsonPropertyName("name")]
|
|
||||||
public string Name { get; set; } = null!;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The game description
|
|
||||||
/// </summary>
|
|
||||||
[JsonPropertyName("description")]
|
|
||||||
public string Description { get; set; } = null!;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The game name in the source language, if different from the returned name.
|
|
||||||
/// </summary>
|
|
||||||
[JsonPropertyName("sourceName")]
|
|
||||||
public string SourceName { get; set; } = null!;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The game description in the source language, if different from the returned description.
|
|
||||||
/// </summary>
|
|
||||||
[JsonPropertyName("sourceDescription")]
|
|
||||||
public string SourceDescription { get; set; } = null!;
|
|
||||||
|
|
||||||
[JsonPropertyName("creator")]
|
|
||||||
public GameCreator Creator { get; set; } = null!;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The game paid access price
|
|
||||||
/// </summary>
|
|
||||||
[JsonPropertyName("price")]
|
|
||||||
public long? Price { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The game allowed gear genres
|
|
||||||
/// </summary>
|
|
||||||
[JsonPropertyName("allowedGearGenres")]
|
|
||||||
public IEnumerable<string> AllowedGearGenres { get; set; } = null!;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The game allowed gear categoris
|
|
||||||
/// </summary>
|
|
||||||
[JsonPropertyName("allowedGearCategories")]
|
|
||||||
public IEnumerable<string> AllowedGearCategories { get; set; } = null!;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The game allows place to be copied
|
|
||||||
/// </summary>
|
|
||||||
[JsonPropertyName("isGenreEnforced")]
|
|
||||||
public bool IsGenreEnforced { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The game allows place to be copied
|
|
||||||
/// </summary>
|
|
||||||
[JsonPropertyName("copyingAllowed")]
|
|
||||||
public bool CopyingAllowed { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Current player count of the game
|
|
||||||
/// </summary>
|
|
||||||
[JsonPropertyName("playing")]
|
|
||||||
public long Playing { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The total visits to the game
|
|
||||||
/// </summary>
|
|
||||||
[JsonPropertyName("visits")]
|
|
||||||
public long Visits { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The game max players
|
|
||||||
/// </summary>
|
|
||||||
[JsonPropertyName("maxPlayers")]
|
|
||||||
public int MaxPlayers { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The game created time
|
|
||||||
/// </summary>
|
|
||||||
[JsonPropertyName("created")]
|
|
||||||
public DateTime Created { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The game updated time
|
|
||||||
/// </summary>
|
|
||||||
[JsonPropertyName("updated")]
|
|
||||||
public DateTime Updated { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The setting of IsStudioAccessToApisAllowed of the universe
|
|
||||||
/// </summary>
|
|
||||||
[JsonPropertyName("studioAccessToApisAllowed")]
|
|
||||||
public bool StudioAccessToApisAllowed { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets the flag to indicate whether the create vip servers button should be allowed in game details page
|
|
||||||
/// </summary>
|
|
||||||
[JsonPropertyName("createVipServersAllowed")]
|
|
||||||
public bool CreateVipServersAllowed { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Avatar type. Possible values are MorphToR6, MorphToR15, and PlayerChoice ['MorphToR6' = 1, 'PlayerChoice' = 2, 'MorphToR15' = 3]
|
|
||||||
/// </summary>
|
|
||||||
[JsonPropertyName("universeAvatarType")]
|
|
||||||
public string UniverseAvatarType { get; set; } = null!;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The game genre display name
|
|
||||||
/// </summary>
|
|
||||||
[JsonPropertyName("genre")]
|
|
||||||
public string Genre { get; set; } = null!;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Is this game all genre.
|
|
||||||
/// </summary>
|
|
||||||
[JsonPropertyName("isAllGenre")]
|
|
||||||
public bool IsAllGenre { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Is this game favorited by user.
|
|
||||||
/// </summary>
|
|
||||||
[JsonPropertyName("isFavoritedByUser")]
|
|
||||||
public bool IsFavoritedByUser { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Game number of favorites.
|
|
||||||
/// </summary>
|
|
||||||
[JsonPropertyName("favoritedCount")]
|
|
||||||
public int FavoritedCount { get; set; }
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,56 +0,0 @@
|
|||||||
namespace Bloxstrap.Models.RobloxApi
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Roblox.Users.Api.GetUserResponse
|
|
||||||
/// </summary>
|
|
||||||
public class GetUserResponse
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// The user description.
|
|
||||||
/// </summary>
|
|
||||||
[JsonPropertyName("description")]
|
|
||||||
public string Description { get; set; } = null!;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// When the user signed up.
|
|
||||||
/// </summary>
|
|
||||||
[JsonPropertyName("created")]
|
|
||||||
public DateTime Created { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Whether the user is banned
|
|
||||||
/// </summary>
|
|
||||||
[JsonPropertyName("isBanned")]
|
|
||||||
public bool IsBanned { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Unused, legacy attribute… rely on its existence.
|
|
||||||
/// </summary>
|
|
||||||
[JsonPropertyName("externalAppDisplayName")]
|
|
||||||
public string ExternalAppDisplayName { get; set; } = null!;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The user's verified badge status.
|
|
||||||
/// </summary>
|
|
||||||
[JsonPropertyName("hasVerifiedBadge")]
|
|
||||||
public bool HasVerifiedBadge { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The user Id.
|
|
||||||
/// </summary>
|
|
||||||
[JsonPropertyName("id")]
|
|
||||||
public long Id { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The user name.
|
|
||||||
/// </summary>
|
|
||||||
[JsonPropertyName("name")]
|
|
||||||
public string Name { get; set; } = null!;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The user DisplayName.
|
|
||||||
/// </summary>
|
|
||||||
[JsonPropertyName("displayName")]
|
|
||||||
public string DisplayName { get; set; } = null!;
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,17 +0,0 @@
|
|||||||
namespace Bloxstrap.Models.APIs.Roblox
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Roblox.Web.Responses.Thumbnails.ThumbnailResponse
|
|
||||||
/// </summary>
|
|
||||||
public class ThumbnailResponse
|
|
||||||
{
|
|
||||||
[JsonPropertyName("targetId")]
|
|
||||||
public long TargetId { get; set; }
|
|
||||||
|
|
||||||
[JsonPropertyName("state")]
|
|
||||||
public string State { get; set; } = null!;
|
|
||||||
|
|
||||||
[JsonPropertyName("imageUrl")]
|
|
||||||
public string ImageUrl { get; set; } = null!;
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,9 +0,0 @@
|
|||||||
namespace Bloxstrap.Models.APIs.Roblox
|
|
||||||
{
|
|
||||||
// lmao its just one property
|
|
||||||
public class UniverseIdResponse
|
|
||||||
{
|
|
||||||
[JsonPropertyName("universeId")]
|
|
||||||
public long UniverseId { get; set; }
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,19 +0,0 @@
|
|||||||
namespace Bloxstrap.Models.Attributes
|
|
||||||
{
|
|
||||||
[AttributeUsage(AttributeTargets.Assembly)]
|
|
||||||
public class BuildMetadataAttribute : Attribute
|
|
||||||
{
|
|
||||||
public DateTime Timestamp { get; set; }
|
|
||||||
public string Machine { get; set; }
|
|
||||||
public string CommitHash { get; set; }
|
|
||||||
public string CommitRef { get; set; }
|
|
||||||
|
|
||||||
public BuildMetadataAttribute(string timestamp, string machine, string commitHash, string commitRef)
|
|
||||||
{
|
|
||||||
Timestamp = DateTime.Parse(timestamp).ToLocalTime();
|
|
||||||
Machine = machine;
|
|
||||||
CommitHash = commitHash;
|
|
||||||
CommitRef = commitRef;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,14 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Text;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
namespace Bloxstrap.Models.Attributes
|
|
||||||
{
|
|
||||||
class EnumNameAttribute : Attribute
|
|
||||||
{
|
|
||||||
public string? StaticName { get; set; }
|
|
||||||
public string? FromTranslation { get; set; }
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,13 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Text;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
namespace Bloxstrap.Models.Attributes
|
|
||||||
{
|
|
||||||
class EnumSortAttribute : Attribute
|
|
||||||
{
|
|
||||||
public int Order { get; set; }
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,10 +0,0 @@
|
|||||||
namespace Bloxstrap.Models.BloxstrapRPC;
|
|
||||||
|
|
||||||
public class Message
|
|
||||||
{
|
|
||||||
[JsonPropertyName("command")]
|
|
||||||
public string Command { get; set; } = null!;
|
|
||||||
|
|
||||||
[JsonPropertyName("data")]
|
|
||||||
public JsonElement Data { get; set; }
|
|
||||||
}
|
|
@ -1,23 +0,0 @@
|
|||||||
namespace Bloxstrap.Models.BloxstrapRPC
|
|
||||||
{
|
|
||||||
class RichPresence
|
|
||||||
{
|
|
||||||
[JsonPropertyName("details")]
|
|
||||||
public string? Details { get; set; }
|
|
||||||
|
|
||||||
[JsonPropertyName("state")]
|
|
||||||
public string? State { get; set; }
|
|
||||||
|
|
||||||
[JsonPropertyName("timeStart")]
|
|
||||||
public ulong? TimestampStart { get; set; }
|
|
||||||
|
|
||||||
[JsonPropertyName("timeEnd")]
|
|
||||||
public ulong? TimestampEnd { get; set; }
|
|
||||||
|
|
||||||
[JsonPropertyName("smallImage")]
|
|
||||||
public RichPresenceImage? SmallImage { get; set; }
|
|
||||||
|
|
||||||
[JsonPropertyName("largeImage")]
|
|
||||||
public RichPresenceImage? LargeImage { get; set; }
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,17 +0,0 @@
|
|||||||
namespace Bloxstrap.Models.BloxstrapRPC
|
|
||||||
{
|
|
||||||
class RichPresenceImage
|
|
||||||
{
|
|
||||||
[JsonPropertyName("assetId")]
|
|
||||||
public ulong? AssetId { get; set; }
|
|
||||||
|
|
||||||
[JsonPropertyName("hoverText")]
|
|
||||||
public string? HoverText { get; set; }
|
|
||||||
|
|
||||||
[JsonPropertyName("clear")]
|
|
||||||
public bool Clear { get; set; } = false;
|
|
||||||
|
|
||||||
[JsonPropertyName("reset")]
|
|
||||||
public bool Reset { get; set; } = false;
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,10 +0,0 @@
|
|||||||
using System.Windows.Media;
|
|
||||||
|
|
||||||
namespace Bloxstrap.Models
|
|
||||||
{
|
|
||||||
public class BootstrapperIconEntry
|
|
||||||
{
|
|
||||||
public BootstrapperIcon IconType { get; set; }
|
|
||||||
public ImageSource ImageSource => IconType.GetIcon().GetImageSource();
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,10 +0,0 @@
|
|||||||
namespace Bloxstrap.Models
|
|
||||||
{
|
|
||||||
public class CustomIntegration
|
|
||||||
{
|
|
||||||
public string Name { get; set; } = "";
|
|
||||||
public string Location { get; set; } = "";
|
|
||||||
public string LaunchArgs { get; set; } = "";
|
|
||||||
public bool AutoClose { get; set; } = true;
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,9 +0,0 @@
|
|||||||
namespace Bloxstrap.Models
|
|
||||||
{
|
|
||||||
public class DeployInfo
|
|
||||||
{
|
|
||||||
public string Timestamp { get; set; } = null!;
|
|
||||||
public string Version { get; set; } = null!;
|
|
||||||
public string VersionGuid { get; set; } = null!;
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,158 +0,0 @@
|
|||||||
using System.Web;
|
|
||||||
using System.Windows;
|
|
||||||
using System.Windows.Input;
|
|
||||||
using Bloxstrap.AppData;
|
|
||||||
using Bloxstrap.Models.APIs;
|
|
||||||
using CommunityToolkit.Mvvm.Input;
|
|
||||||
|
|
||||||
namespace Bloxstrap.Models.Entities
|
|
||||||
{
|
|
||||||
public class ActivityData
|
|
||||||
{
|
|
||||||
private long _universeId = 0;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// If the current activity stems from an in-universe teleport, then this will be
|
|
||||||
/// set to the activity that corresponds to the initial game join
|
|
||||||
/// </summary>
|
|
||||||
public ActivityData? RootActivity;
|
|
||||||
|
|
||||||
public long UniverseId
|
|
||||||
{
|
|
||||||
get => _universeId;
|
|
||||||
set
|
|
||||||
{
|
|
||||||
_universeId = value;
|
|
||||||
UniverseDetails.LoadFromCache(value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public long PlaceId { get; set; } = 0;
|
|
||||||
|
|
||||||
public string JobId { get; set; } = string.Empty;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// This will be empty unless the server joined is a private server
|
|
||||||
/// </summary>
|
|
||||||
public string AccessCode { get; set; } = string.Empty;
|
|
||||||
|
|
||||||
public long UserId { get; set; } = 0;
|
|
||||||
|
|
||||||
public string MachineAddress { get; set; } = string.Empty;
|
|
||||||
|
|
||||||
public bool MachineAddressValid => !string.IsNullOrEmpty(MachineAddress) && !MachineAddress.StartsWith("10.");
|
|
||||||
|
|
||||||
public bool IsTeleport { get; set; } = false;
|
|
||||||
|
|
||||||
public ServerType ServerType { get; set; } = ServerType.Public;
|
|
||||||
|
|
||||||
public DateTime TimeJoined { get; set; }
|
|
||||||
|
|
||||||
public DateTime? TimeLeft { get; set; }
|
|
||||||
|
|
||||||
// everything below here is optional strictly for bloxstraprpc, discord rich presence, or game history
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// This is intended only for other people to use, i.e. context menu invite link, rich presence joining
|
|
||||||
/// </summary>
|
|
||||||
public string RPCLaunchData { get; set; } = string.Empty;
|
|
||||||
|
|
||||||
public UniverseDetails? UniverseDetails { get; set; }
|
|
||||||
|
|
||||||
public string GameHistoryDescription
|
|
||||||
{
|
|
||||||
get
|
|
||||||
{
|
|
||||||
string desc = string.Format(
|
|
||||||
"{0} • {1} {2} {3}",
|
|
||||||
UniverseDetails?.Data.Creator.Name,
|
|
||||||
TimeJoined.ToString("t"),
|
|
||||||
Locale.CurrentCulture.Name.StartsWith("ja") ? '~' : '-',
|
|
||||||
TimeLeft?.ToString("t")
|
|
||||||
);
|
|
||||||
|
|
||||||
if (ServerType != ServerType.Public)
|
|
||||||
desc += " • " + ServerType.ToTranslatedString();
|
|
||||||
|
|
||||||
return desc;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public ICommand RejoinServerCommand => new RelayCommand(RejoinServer);
|
|
||||||
|
|
||||||
private SemaphoreSlim serverQuerySemaphore = new(1, 1);
|
|
||||||
|
|
||||||
public string GetInviteDeeplink(bool launchData = true)
|
|
||||||
{
|
|
||||||
string deeplink = $"roblox://experiences/start?placeId={PlaceId}";
|
|
||||||
|
|
||||||
if (ServerType == ServerType.Private)
|
|
||||||
deeplink += "&accessCode=" + AccessCode;
|
|
||||||
else
|
|
||||||
deeplink += "&gameInstanceId=" + JobId;
|
|
||||||
|
|
||||||
if (launchData && !string.IsNullOrEmpty(RPCLaunchData))
|
|
||||||
deeplink += "&launchData=" + HttpUtility.UrlEncode(RPCLaunchData);
|
|
||||||
|
|
||||||
return deeplink;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<string?> QueryServerLocation()
|
|
||||||
{
|
|
||||||
const string LOG_IDENT = "ActivityData::QueryServerLocation";
|
|
||||||
|
|
||||||
if (!MachineAddressValid)
|
|
||||||
throw new InvalidOperationException($"Machine address is invalid ({MachineAddress})");
|
|
||||||
|
|
||||||
await serverQuerySemaphore.WaitAsync();
|
|
||||||
|
|
||||||
if (GlobalCache.ServerLocation.TryGetValue(MachineAddress, out string? location))
|
|
||||||
{
|
|
||||||
serverQuerySemaphore.Release();
|
|
||||||
return location;
|
|
||||||
}
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var ipInfo = await Http.GetJson<IPInfoResponse>($"https://ipinfo.io/{MachineAddress}/json");
|
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(ipInfo.City))
|
|
||||||
throw new InvalidHTTPResponseException("Reported city was blank");
|
|
||||||
|
|
||||||
if (ipInfo.City == ipInfo.Region)
|
|
||||||
location = $"{ipInfo.Region}, {ipInfo.Country}";
|
|
||||||
else
|
|
||||||
location = $"{ipInfo.City}, {ipInfo.Region}, {ipInfo.Country}";
|
|
||||||
|
|
||||||
GlobalCache.ServerLocation[MachineAddress] = location;
|
|
||||||
serverQuerySemaphore.Release();
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
App.Logger.WriteLine(LOG_IDENT, $"Failed to get server location for {MachineAddress}");
|
|
||||||
App.Logger.WriteException(LOG_IDENT, ex);
|
|
||||||
|
|
||||||
GlobalCache.ServerLocation[MachineAddress] = location;
|
|
||||||
serverQuerySemaphore.Release();
|
|
||||||
|
|
||||||
Frontend.ShowConnectivityDialog(
|
|
||||||
string.Format(Strings.Dialog_Connectivity_UnableToConnect, "ipinfo.io"),
|
|
||||||
Strings.ActivityWatcher_LocationQueryFailed,
|
|
||||||
MessageBoxImage.Warning,
|
|
||||||
ex
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return location;
|
|
||||||
}
|
|
||||||
|
|
||||||
public override string ToString() => $"{PlaceId}/{JobId}";
|
|
||||||
|
|
||||||
private void RejoinServer()
|
|
||||||
{
|
|
||||||
string playerPath = new RobloxPlayerData().ExecutablePath;
|
|
||||||
|
|
||||||
Process.Start(playerPath, GetInviteDeeplink(false));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,40 +0,0 @@
|
|||||||
using System.Security.Cryptography;
|
|
||||||
using System.Windows.Markup;
|
|
||||||
|
|
||||||
namespace Bloxstrap.Models.Entities
|
|
||||||
{
|
|
||||||
public class ModPresetFileData
|
|
||||||
{
|
|
||||||
public string FilePath { get; private set; }
|
|
||||||
|
|
||||||
public string FullFilePath => Path.Combine(Paths.Modifications, FilePath);
|
|
||||||
|
|
||||||
public FileStream FileStream => File.OpenRead(FullFilePath);
|
|
||||||
|
|
||||||
public string ResourceIdentifier { get; private set; }
|
|
||||||
|
|
||||||
public Stream ResourceStream => Resource.GetStream(ResourceIdentifier);
|
|
||||||
|
|
||||||
public byte[] ResourceHash { get; private set; }
|
|
||||||
|
|
||||||
public ModPresetFileData(string contentPath, string resource)
|
|
||||||
{
|
|
||||||
FilePath = contentPath;
|
|
||||||
ResourceIdentifier = resource;
|
|
||||||
|
|
||||||
using var stream = ResourceStream;
|
|
||||||
ResourceHash = App.MD5Provider.ComputeHash(stream);
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool HashMatches()
|
|
||||||
{
|
|
||||||
if (!File.Exists(FullFilePath))
|
|
||||||
return false;
|
|
||||||
|
|
||||||
using var fileStream = FileStream;
|
|
||||||
var fileHash = App.MD5Provider.ComputeHash(fileStream);
|
|
||||||
|
|
||||||
return fileHash.SequenceEqual(ResourceHash);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,53 +0,0 @@
|
|||||||
namespace Bloxstrap.Models.Entities
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Explicit loading. Load from cache before and after a fetch.
|
|
||||||
/// </summary>
|
|
||||||
public class UniverseDetails
|
|
||||||
{
|
|
||||||
private static List<UniverseDetails> _cache { get; set; } = new();
|
|
||||||
|
|
||||||
public GameDetailResponse Data { get; set; } = null!;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Returns data for a 128x128 icon
|
|
||||||
/// </summary>
|
|
||||||
public ThumbnailResponse Thumbnail { get; set; } = null!;
|
|
||||||
|
|
||||||
public static UniverseDetails? LoadFromCache(long id)
|
|
||||||
{
|
|
||||||
var cacheQuery = _cache.Where(x => x.Data?.Id == id);
|
|
||||||
|
|
||||||
if (cacheQuery.Any())
|
|
||||||
return cacheQuery.First();
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static Task FetchSingle(long id) => FetchBulk(id.ToString());
|
|
||||||
|
|
||||||
public static async Task FetchBulk(string ids)
|
|
||||||
{
|
|
||||||
var gameDetailResponse = await Http.GetJson<ApiArrayResponse<GameDetailResponse>>($"https://games.roblox.com/v1/games?universeIds={ids}");
|
|
||||||
|
|
||||||
if (!gameDetailResponse.Data.Any())
|
|
||||||
throw new InvalidHTTPResponseException("Roblox API for Game Details returned invalid data");
|
|
||||||
|
|
||||||
var universeThumbnailResponse = await Http.GetJson<ApiArrayResponse<ThumbnailResponse>>($"https://thumbnails.roblox.com/v1/games/icons?universeIds={ids}&returnPolicy=PlaceHolder&size=128x128&format=Png&isCircular=false");
|
|
||||||
|
|
||||||
if (!universeThumbnailResponse.Data.Any())
|
|
||||||
throw new InvalidHTTPResponseException("Roblox API for Game Thumbnails returned invalid data");
|
|
||||||
|
|
||||||
foreach (string strId in ids.Split(','))
|
|
||||||
{
|
|
||||||
long id = long.Parse(strId);
|
|
||||||
|
|
||||||
_cache.Add(new UniverseDetails
|
|
||||||
{
|
|
||||||
Data = gameDetailResponse.Data.Where(x => x.Id == id).First(),
|
|
||||||
Thumbnail = universeThumbnailResponse.Data.Where(x => x.TargetId == id).First(),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,42 +0,0 @@
|
|||||||
using Bloxstrap.Models.RobloxApi;
|
|
||||||
|
|
||||||
namespace Bloxstrap.Models.Entities
|
|
||||||
{
|
|
||||||
public class UserDetails
|
|
||||||
{
|
|
||||||
private static List<UserDetails> _cache { get; set; } = new();
|
|
||||||
|
|
||||||
public GetUserResponse Data { get; set; } = null!;
|
|
||||||
|
|
||||||
public ThumbnailResponse Thumbnail { get; set; } = null!;
|
|
||||||
|
|
||||||
public static async Task<UserDetails> Fetch(long id)
|
|
||||||
{
|
|
||||||
var cacheQuery = _cache.Where(x => x.Data?.Id == id);
|
|
||||||
|
|
||||||
if (cacheQuery.Any())
|
|
||||||
return cacheQuery.First();
|
|
||||||
|
|
||||||
var userResponse = await Http.GetJson<GetUserResponse>($"https://users.roblox.com/v1/users/{id}");
|
|
||||||
|
|
||||||
if (userResponse is null)
|
|
||||||
throw new InvalidHTTPResponseException("Roblox API for User Details returned invalid data");
|
|
||||||
|
|
||||||
// we can remove '-headshot' from the url if we want a full avatar picture
|
|
||||||
var thumbnailResponse = await Http.GetJson<ApiArrayResponse<ThumbnailResponse>>($"https://thumbnails.roblox.com/v1/users/avatar-headshot?userIds={id}&size=180x180&format=Png&isCircular=false");
|
|
||||||
|
|
||||||
if (thumbnailResponse is null || !thumbnailResponse.Data.Any())
|
|
||||||
throw new InvalidHTTPResponseException("Roblox API for Thumbnails returned invalid data");
|
|
||||||
|
|
||||||
var details = new UserDetails
|
|
||||||
{
|
|
||||||
Data = userResponse,
|
|
||||||
Thumbnail = thumbnailResponse.Data.First()
|
|
||||||
};
|
|
||||||
|
|
||||||
_cache.Add(details);
|
|
||||||
|
|
||||||
return details;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,9 +0,0 @@
|
|||||||
namespace Bloxstrap.Models
|
|
||||||
{
|
|
||||||
public class FastFlag
|
|
||||||
{
|
|
||||||
// public bool Enabled { get; set; }
|
|
||||||
public string Name { get; set; } = null!;
|
|
||||||
public string Value { get; set; } = null!;
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,17 +0,0 @@
|
|||||||
namespace Bloxstrap.Models
|
|
||||||
{
|
|
||||||
public class FontFace
|
|
||||||
{
|
|
||||||
[JsonPropertyName("name")]
|
|
||||||
public string Name { get; set; } = null!;
|
|
||||||
|
|
||||||
[JsonPropertyName("weight")]
|
|
||||||
public int Weight { get; set; }
|
|
||||||
|
|
||||||
[JsonPropertyName("style")]
|
|
||||||
public string Style { get; set; } = null!;
|
|
||||||
|
|
||||||
[JsonPropertyName("assetId")]
|
|
||||||
public string AssetId { get; set; } = null!;
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,11 +0,0 @@
|
|||||||
namespace Bloxstrap.Models
|
|
||||||
{
|
|
||||||
public class FontFamily
|
|
||||||
{
|
|
||||||
[JsonPropertyName("name")]
|
|
||||||
public string Name { get; set; } = null!;
|
|
||||||
|
|
||||||
[JsonPropertyName("faces")]
|
|
||||||
public IEnumerable<FontFace> Faces { get; set; } = null!;
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,22 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Text;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
namespace Bloxstrap.Models
|
|
||||||
{
|
|
||||||
public class LaunchFlag
|
|
||||||
{
|
|
||||||
public string Identifiers { get; private set; }
|
|
||||||
|
|
||||||
public bool Active = false;
|
|
||||||
|
|
||||||
public string? Data;
|
|
||||||
|
|
||||||
public LaunchFlag(string identifiers)
|
|
||||||
{
|
|
||||||
Identifiers = identifiers;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,35 +0,0 @@
|
|||||||
using Bloxstrap.RobloxInterfaces;
|
|
||||||
|
|
||||||
namespace Bloxstrap.Models.Manifest
|
|
||||||
{
|
|
||||||
public class FileManifest : List<ManifestFile>
|
|
||||||
{
|
|
||||||
private FileManifest(string data)
|
|
||||||
{
|
|
||||||
using StringReader reader = new StringReader(data);
|
|
||||||
|
|
||||||
while (true)
|
|
||||||
{
|
|
||||||
string? fileName = reader.ReadLine();
|
|
||||||
string? signature = reader.ReadLine();
|
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(fileName) || string.IsNullOrEmpty(signature))
|
|
||||||
break;
|
|
||||||
|
|
||||||
Add(new ManifestFile
|
|
||||||
{
|
|
||||||
Name = fileName,
|
|
||||||
Signature = signature
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static async Task<FileManifest> Get(string versionGuid)
|
|
||||||
{
|
|
||||||
string pkgManifestUrl = Deployment.GetLocation($"/{versionGuid}-rbxManifest.txt");
|
|
||||||
var pkgManifestData = await App.HttpClient.GetStringAsync(pkgManifestUrl);
|
|
||||||
|
|
||||||
return new FileManifest(pkgManifestData);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,13 +0,0 @@
|
|||||||
namespace Bloxstrap.Models.Manifest
|
|
||||||
{
|
|
||||||
public class ManifestFile
|
|
||||||
{
|
|
||||||
public string Name { get; set; } = "";
|
|
||||||
public string Signature { get; set; } = "";
|
|
||||||
|
|
||||||
public override string ToString()
|
|
||||||
{
|
|
||||||
return $"[{Signature}] {Name}";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,26 +0,0 @@
|
|||||||
/*
|
|
||||||
* Roblox Studio Mod Manager (ProjectSrc/Utility/Package.cs)
|
|
||||||
* MIT License
|
|
||||||
* Copyright (c) 2015-present MaximumADHD
|
|
||||||
*/
|
|
||||||
|
|
||||||
namespace Bloxstrap.Models.Manifest
|
|
||||||
{
|
|
||||||
public class Package
|
|
||||||
{
|
|
||||||
public string Name { get; set; } = "";
|
|
||||||
|
|
||||||
public string Signature { get; set; } = "";
|
|
||||||
|
|
||||||
public int PackedSize { get; set; }
|
|
||||||
|
|
||||||
public int Size { get; set; }
|
|
||||||
|
|
||||||
public string DownloadPath => Path.Combine(Paths.Downloads, Signature);
|
|
||||||
|
|
||||||
public override string ToString()
|
|
||||||
{
|
|
||||||
return $"[{Signature}] {Name}";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,50 +0,0 @@
|
|||||||
/*
|
|
||||||
* Roblox Studio Mod Manager (ProjectSrc/Utility/PackageManifest.cs)
|
|
||||||
* MIT License
|
|
||||||
* Copyright (c) 2015-present MaximumADHD
|
|
||||||
*/
|
|
||||||
|
|
||||||
namespace Bloxstrap.Models.Manifest
|
|
||||||
{
|
|
||||||
public class PackageManifest : List<Package>
|
|
||||||
{
|
|
||||||
public PackageManifest(string data)
|
|
||||||
{
|
|
||||||
using var reader = new StringReader(data);
|
|
||||||
string? version = reader.ReadLine();
|
|
||||||
|
|
||||||
if (version != "v0")
|
|
||||||
throw new NotSupportedException($"Unexpected package manifest version: {version} (expected v0!)");
|
|
||||||
|
|
||||||
while (true)
|
|
||||||
{
|
|
||||||
string? fileName = reader.ReadLine();
|
|
||||||
string? signature = reader.ReadLine();
|
|
||||||
|
|
||||||
string? rawPackedSize = reader.ReadLine();
|
|
||||||
string? rawSize = reader.ReadLine();
|
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(fileName) ||
|
|
||||||
string.IsNullOrEmpty(signature) ||
|
|
||||||
string.IsNullOrEmpty(rawPackedSize) ||
|
|
||||||
string.IsNullOrEmpty(rawSize))
|
|
||||||
break;
|
|
||||||
|
|
||||||
// ignore launcher
|
|
||||||
if (fileName == "RobloxPlayerLauncher.exe")
|
|
||||||
break;
|
|
||||||
|
|
||||||
int packedSize = int.Parse(rawPackedSize);
|
|
||||||
int size = int.Parse(rawSize);
|
|
||||||
|
|
||||||
Add(new Package
|
|
||||||
{
|
|
||||||
Name = fileName,
|
|
||||||
Signature = signature,
|
|
||||||
PackedSize = packedSize,
|
|
||||||
Size = size
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,11 +0,0 @@
|
|||||||
namespace Bloxstrap.Models.Persistable
|
|
||||||
{
|
|
||||||
public class AppState
|
|
||||||
{
|
|
||||||
public string VersionGuid { get; set; } = string.Empty;
|
|
||||||
|
|
||||||
public Dictionary<string, string> PackageHashes { get; set; } = new();
|
|
||||||
|
|
||||||
public int Size { get; set; }
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,32 +0,0 @@
|
|||||||
using System.Collections.ObjectModel;
|
|
||||||
|
|
||||||
namespace Bloxstrap.Models.Persistable
|
|
||||||
{
|
|
||||||
public class Settings
|
|
||||||
{
|
|
||||||
// bloxstrap configuration
|
|
||||||
public BootstrapperStyle BootstrapperStyle { get; set; } = BootstrapperStyle.FluentDialog;
|
|
||||||
public BootstrapperIcon BootstrapperIcon { get; set; } = BootstrapperIcon.IconBloxstrap;
|
|
||||||
public string BootstrapperTitle { get; set; } = App.ProjectName;
|
|
||||||
public string BootstrapperIconCustomLocation { get; set; } = "";
|
|
||||||
public Theme Theme { get; set; } = Theme.Default;
|
|
||||||
public bool CheckForUpdates { get; set; } = true;
|
|
||||||
public bool ConfirmLaunches { get; set; } = false;
|
|
||||||
public string Locale { get; set; } = "nil";
|
|
||||||
public bool ForceRobloxLanguage { get; set; } = false;
|
|
||||||
public bool UseFastFlagManager { get; set; } = true;
|
|
||||||
public bool WPFSoftwareRender { get; set; } = false;
|
|
||||||
public bool EnableAnalytics { get; set; } = true;
|
|
||||||
|
|
||||||
// integration configuration
|
|
||||||
public bool EnableActivityTracking { get; set; } = true;
|
|
||||||
public bool UseDiscordRichPresence { get; set; } = true;
|
|
||||||
public bool HideRPCButtons { get; set; } = true;
|
|
||||||
public bool ShowAccountOnRichPresence { get; set; } = false;
|
|
||||||
public bool ShowServerDetails { get; set; } = false;
|
|
||||||
public ObservableCollection<CustomIntegration> CustomIntegrations { get; set; } = new();
|
|
||||||
|
|
||||||
// mod preset configuration
|
|
||||||
public bool UseDisableAppPatch { get; set; } = false;
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,17 +0,0 @@
|
|||||||
namespace Bloxstrap.Models.Persistable
|
|
||||||
{
|
|
||||||
public class State
|
|
||||||
{
|
|
||||||
public bool ShowFFlagEditorWarning { get; set; } = true;
|
|
||||||
|
|
||||||
public bool PromptWebView2Install { get; set; } = true;
|
|
||||||
|
|
||||||
public AppState Player { get; set; } = new();
|
|
||||||
|
|
||||||
public AppState Studio { get; set; } = new();
|
|
||||||
|
|
||||||
public WindowState SettingsWindow { get; set; } = new();
|
|
||||||
|
|
||||||
public List<string> ModManifest { get; set; } = new();
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,13 +0,0 @@
|
|||||||
namespace Bloxstrap.Models.Persistable
|
|
||||||
{
|
|
||||||
public class WindowState
|
|
||||||
{
|
|
||||||
public double Width { get; set; }
|
|
||||||
|
|
||||||
public double Height { get; set; }
|
|
||||||
|
|
||||||
public double Left { get; set; }
|
|
||||||
|
|
||||||
public double Top { get; set; }
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,23 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Text;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
namespace Bloxstrap.Models.SettingTasks.Base
|
|
||||||
{
|
|
||||||
public abstract class BaseTask
|
|
||||||
{
|
|
||||||
public string Name { get; private set; }
|
|
||||||
|
|
||||||
public abstract bool Changed { get; }
|
|
||||||
|
|
||||||
public BaseTask(string prefix, string name) : this($"{prefix}.{name}") { }
|
|
||||||
|
|
||||||
public BaseTask(string name) => Name = name;
|
|
||||||
|
|
||||||
public override string ToString() => Name;
|
|
||||||
|
|
||||||
public abstract void Execute();
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,47 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Text;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
namespace Bloxstrap.Models.SettingTasks.Base
|
|
||||||
{
|
|
||||||
public abstract class BoolBaseTask : BaseTask
|
|
||||||
{
|
|
||||||
private bool _originalState;
|
|
||||||
|
|
||||||
private bool _newState;
|
|
||||||
|
|
||||||
public virtual bool OriginalState
|
|
||||||
{
|
|
||||||
get => _originalState;
|
|
||||||
|
|
||||||
set
|
|
||||||
{
|
|
||||||
_originalState = value;
|
|
||||||
_newState = value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public virtual bool NewState
|
|
||||||
{
|
|
||||||
get => _newState;
|
|
||||||
|
|
||||||
set
|
|
||||||
{
|
|
||||||
_newState = value;
|
|
||||||
|
|
||||||
if (Changed)
|
|
||||||
App.PendingSettingTasks[Name] = this;
|
|
||||||
else
|
|
||||||
App.PendingSettingTasks.Remove(Name);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public override bool Changed => _newState != OriginalState;
|
|
||||||
|
|
||||||
public BoolBaseTask(string prefix, string name) : base(prefix, name) { }
|
|
||||||
|
|
||||||
public BoolBaseTask(string name) : base(name) { }
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,53 +0,0 @@
|
|||||||
namespace Bloxstrap.Models.SettingTasks.Base
|
|
||||||
{
|
|
||||||
public abstract class EnumBaseTask<T> : BaseTask where T : struct, Enum
|
|
||||||
{
|
|
||||||
private T _originalState = default!;
|
|
||||||
|
|
||||||
private T _newState = default!;
|
|
||||||
|
|
||||||
public virtual T OriginalState
|
|
||||||
{
|
|
||||||
get => _originalState;
|
|
||||||
|
|
||||||
set
|
|
||||||
{
|
|
||||||
_originalState = value;
|
|
||||||
_newState = value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public virtual T NewState
|
|
||||||
{
|
|
||||||
get => _newState;
|
|
||||||
|
|
||||||
set
|
|
||||||
{
|
|
||||||
_newState = value;
|
|
||||||
|
|
||||||
if (Changed)
|
|
||||||
App.PendingSettingTasks[Name] = this;
|
|
||||||
else
|
|
||||||
App.PendingSettingTasks.Remove(Name);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public override bool Changed => !_newState.Equals(OriginalState);
|
|
||||||
|
|
||||||
public IEnumerable<T> Selections { get; private set; }
|
|
||||||
= Enum.GetValues(typeof(T)).Cast<T>().OrderBy(x =>
|
|
||||||
{
|
|
||||||
var attributes = x.GetType().GetMember(x.ToString())[0].GetCustomAttributes(typeof(EnumSortAttribute), false);
|
|
||||||
|
|
||||||
if (attributes.Length > 0)
|
|
||||||
{
|
|
||||||
var attribute = (EnumSortAttribute)attributes[0];
|
|
||||||
return attribute.Order;
|
|
||||||
}
|
|
||||||
|
|
||||||
return 0;
|
|
||||||
});
|
|
||||||
|
|
||||||
public EnumBaseTask(string prefix, string name) : base(prefix, name) { }
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,45 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Text;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
namespace Bloxstrap.Models.SettingTasks.Base
|
|
||||||
{
|
|
||||||
public abstract class StringBaseTask : BaseTask
|
|
||||||
{
|
|
||||||
private string _originalState = "";
|
|
||||||
|
|
||||||
private string _newState = "";
|
|
||||||
|
|
||||||
public virtual string OriginalState
|
|
||||||
{
|
|
||||||
get => _originalState;
|
|
||||||
|
|
||||||
set
|
|
||||||
{
|
|
||||||
_originalState = value;
|
|
||||||
_newState = value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public virtual string NewState
|
|
||||||
{
|
|
||||||
get => _newState;
|
|
||||||
|
|
||||||
set
|
|
||||||
{
|
|
||||||
_newState = value;
|
|
||||||
|
|
||||||
if (Changed)
|
|
||||||
App.PendingSettingTasks[Name] = this;
|
|
||||||
else
|
|
||||||
App.PendingSettingTasks.Remove(Name);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public override bool Changed => _newState != OriginalState;
|
|
||||||
|
|
||||||
public StringBaseTask(string prefix, string name) : base(prefix, name) { }
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,72 +0,0 @@
|
|||||||
using System.Windows;
|
|
||||||
|
|
||||||
using Bloxstrap.Models.SettingTasks.Base;
|
|
||||||
|
|
||||||
namespace Bloxstrap.Models.SettingTasks
|
|
||||||
{
|
|
||||||
public class EmojiModPresetTask : EnumBaseTask<EmojiType>
|
|
||||||
{
|
|
||||||
private string _filePath => Path.Combine(Paths.Modifications, @"content\fonts\TwemojiMozilla.ttf");
|
|
||||||
|
|
||||||
private IEnumerable<KeyValuePair<EmojiType, string>>? QueryCurrentValue()
|
|
||||||
{
|
|
||||||
if (!File.Exists(_filePath))
|
|
||||||
return null;
|
|
||||||
|
|
||||||
using var fileStream = File.OpenRead(_filePath);
|
|
||||||
string hash = MD5Hash.Stringify(App.MD5Provider.ComputeHash(fileStream));
|
|
||||||
|
|
||||||
return EmojiTypeEx.Hashes.Where(x => x.Value == hash);
|
|
||||||
}
|
|
||||||
|
|
||||||
public EmojiModPresetTask() : base("ModPreset", "EmojiFont")
|
|
||||||
{
|
|
||||||
var query = QueryCurrentValue();
|
|
||||||
|
|
||||||
if (query is not null)
|
|
||||||
OriginalState = query.FirstOrDefault().Key;
|
|
||||||
}
|
|
||||||
|
|
||||||
public override async void Execute()
|
|
||||||
{
|
|
||||||
const string LOG_IDENT = "EmojiModPresetTask::Execute";
|
|
||||||
|
|
||||||
var query = QueryCurrentValue();
|
|
||||||
|
|
||||||
if (NewState != EmojiType.Default && (query is null || query.FirstOrDefault().Key != NewState))
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var response = await App.HttpClient.GetAsync(NewState.GetUrl());
|
|
||||||
|
|
||||||
response.EnsureSuccessStatusCode();
|
|
||||||
|
|
||||||
Directory.CreateDirectory(Path.GetDirectoryName(_filePath)!);
|
|
||||||
|
|
||||||
await using var fileStream = new FileStream(_filePath, FileMode.Create);
|
|
||||||
await response.Content.CopyToAsync(fileStream);
|
|
||||||
|
|
||||||
OriginalState = NewState;
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
App.Logger.WriteException(LOG_IDENT, ex);
|
|
||||||
|
|
||||||
Frontend.ShowConnectivityDialog(
|
|
||||||
String.Format(Strings.Dialog_Connectivity_UnableToConnect, "GitHub"),
|
|
||||||
$"{Strings.Menu_Mods_Presets_EmojiType_Error}\n\n{Strings.Dialog_Connectivity_TryAgainLater}",
|
|
||||||
MessageBoxImage.Warning,
|
|
||||||
ex
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if (query is not null && query.Any())
|
|
||||||
{
|
|
||||||
Filesystem.AssertReadOnly(_filePath);
|
|
||||||
File.Delete(_filePath);
|
|
||||||
|
|
||||||
OriginalState = NewState;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,69 +0,0 @@
|
|||||||
using Bloxstrap.Models.Entities;
|
|
||||||
using Bloxstrap.Models.SettingTasks.Base;
|
|
||||||
|
|
||||||
namespace Bloxstrap.Models.SettingTasks
|
|
||||||
{
|
|
||||||
public class EnumModPresetTask<T> : EnumBaseTask<T> where T : struct, Enum
|
|
||||||
{
|
|
||||||
private readonly Dictionary<T, Dictionary<string, ModPresetFileData>> _fileDataMap = new();
|
|
||||||
|
|
||||||
private readonly Dictionary<T, Dictionary<string, string>> _map;
|
|
||||||
|
|
||||||
public EnumModPresetTask(string name, Dictionary<T, Dictionary<string, string>> map) : base("ModPreset", name)
|
|
||||||
{
|
|
||||||
_map = map;
|
|
||||||
|
|
||||||
foreach (var enumPair in _map)
|
|
||||||
{
|
|
||||||
var dataMap = new Dictionary<string, ModPresetFileData>();
|
|
||||||
|
|
||||||
foreach (var resourcePair in enumPair.Value)
|
|
||||||
{
|
|
||||||
var data = new ModPresetFileData(resourcePair.Key, resourcePair.Value);
|
|
||||||
|
|
||||||
if (data.HashMatches() && OriginalState.Equals(default(T)))
|
|
||||||
OriginalState = enumPair.Key;
|
|
||||||
|
|
||||||
dataMap[resourcePair.Key] = data;
|
|
||||||
}
|
|
||||||
|
|
||||||
_fileDataMap[enumPair.Key] = dataMap;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public override void Execute()
|
|
||||||
{
|
|
||||||
if (!NewState.Equals(default(T)))
|
|
||||||
{
|
|
||||||
var resourceMap = _fileDataMap[NewState];
|
|
||||||
|
|
||||||
foreach (var resourcePair in resourceMap)
|
|
||||||
{
|
|
||||||
var data = resourcePair.Value;
|
|
||||||
|
|
||||||
if (!data.HashMatches())
|
|
||||||
{
|
|
||||||
Directory.CreateDirectory(Path.GetDirectoryName(data.FullFilePath)!);
|
|
||||||
|
|
||||||
using var resourceStream = data.ResourceStream;
|
|
||||||
using var memoryStream = new MemoryStream();
|
|
||||||
data.ResourceStream.CopyTo(memoryStream);
|
|
||||||
|
|
||||||
Filesystem.AssertReadOnly(data.FullFilePath);
|
|
||||||
File.WriteAllBytes(data.FullFilePath, memoryStream.ToArray());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
foreach (var dataPair in _fileDataMap.First().Value)
|
|
||||||
{
|
|
||||||
Filesystem.AssertReadOnly(dataPair.Value.FullFilePath);
|
|
||||||
File.Delete(dataPair.Value.FullFilePath);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
OriginalState = NewState;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,43 +0,0 @@
|
|||||||
using System.Reflection;
|
|
||||||
|
|
||||||
namespace Bloxstrap.Models.SettingTasks
|
|
||||||
{
|
|
||||||
public class ExtractIconsTask : BoolBaseTask
|
|
||||||
{
|
|
||||||
private string _path => Path.Combine(Paths.Base, Strings.Paths_Icons);
|
|
||||||
|
|
||||||
public ExtractIconsTask() : base("ExtractIcons")
|
|
||||||
{
|
|
||||||
OriginalState = Directory.Exists(_path);
|
|
||||||
}
|
|
||||||
|
|
||||||
public override void Execute()
|
|
||||||
{
|
|
||||||
if (NewState)
|
|
||||||
{
|
|
||||||
Directory.CreateDirectory(_path);
|
|
||||||
|
|
||||||
var assembly = Assembly.GetExecutingAssembly();
|
|
||||||
var resourceNames = assembly.GetManifestResourceNames().Where(x => x.EndsWith(".ico"));
|
|
||||||
|
|
||||||
foreach (string name in resourceNames)
|
|
||||||
{
|
|
||||||
string path = Path.Combine(_path, name.Replace("Bloxstrap.Resources.", ""));
|
|
||||||
var stream = assembly.GetManifestResourceStream(name)!;
|
|
||||||
|
|
||||||
using var memoryStream = new MemoryStream();
|
|
||||||
stream.CopyTo(memoryStream);
|
|
||||||
|
|
||||||
Filesystem.AssertReadOnly(path);
|
|
||||||
File.WriteAllBytes(path, memoryStream.ToArray());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if (Directory.Exists(_path))
|
|
||||||
{
|
|
||||||
Directory.Delete(_path, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
OriginalState = NewState;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,43 +0,0 @@
|
|||||||
using Bloxstrap.Models.SettingTasks.Base;
|
|
||||||
|
|
||||||
namespace Bloxstrap.Models.SettingTasks
|
|
||||||
{
|
|
||||||
public class FontModPresetTask : StringBaseTask
|
|
||||||
{
|
|
||||||
public string? GetFileHash()
|
|
||||||
{
|
|
||||||
if (!File.Exists(Paths.CustomFont))
|
|
||||||
return null;
|
|
||||||
|
|
||||||
using var fileStream = File.OpenRead(Paths.CustomFont);
|
|
||||||
return MD5Hash.Stringify(App.MD5Provider.ComputeHash(fileStream));
|
|
||||||
}
|
|
||||||
|
|
||||||
public FontModPresetTask() : base("ModPreset", "TextFont")
|
|
||||||
{
|
|
||||||
if (File.Exists(Paths.CustomFont))
|
|
||||||
OriginalState = Paths.CustomFont;
|
|
||||||
}
|
|
||||||
|
|
||||||
public override void Execute()
|
|
||||||
{
|
|
||||||
if (!String.IsNullOrEmpty(NewState))
|
|
||||||
{
|
|
||||||
if (String.Compare(NewState, Paths.CustomFont, StringComparison.InvariantCultureIgnoreCase) != 0 && File.Exists(NewState))
|
|
||||||
{
|
|
||||||
Directory.CreateDirectory(Path.GetDirectoryName(Paths.CustomFont)!);
|
|
||||||
|
|
||||||
Filesystem.AssertReadOnly(Paths.CustomFont);
|
|
||||||
File.Copy(NewState, Paths.CustomFont, true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if (File.Exists(Paths.CustomFont))
|
|
||||||
{
|
|
||||||
Filesystem.AssertReadOnly(Paths.CustomFont);
|
|
||||||
File.Delete(Paths.CustomFont);
|
|
||||||
}
|
|
||||||
|
|
||||||
OriginalState = NewState;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user