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