This commit is contained in:
Slenderman 2025-03-07 16:21:49 -05:00
parent 552f2a52a6
commit 8e47290e3d
319 changed files with 1 additions and 58994 deletions

View File

@ -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
View File

@ -0,0 +1 @@
IT WORKS

View File

@ -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>

View File

@ -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");
}
}
}

View File

@ -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;
}
}
}

View File

@ -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; }
}
}

View File

@ -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", @"" }
};
}
}

View File

@ -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\" }
};
}
}

View File

@ -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)
)]

View File

@ -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

View File

@ -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
}
}

View File

@ -1,15 +0,0 @@
namespace Bloxstrap.Enums
{
public enum BootstrapperStyle
{
VistaDialog,
LegacyDialog2008,
LegacyDialog2011,
ProgressDialog,
ClassicFluentDialog,
ByfronDialog,
[EnumName(StaticName = "Bloxstrap")]
FluentDialog,
FluentAeroDialog
}
}

View File

@ -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
}
}

View File

@ -1,11 +0,0 @@
namespace Bloxstrap.Enums
{
public enum EmojiType
{
Default,
Catmoji,
Windows11,
Windows10,
Windows8
}
}

View File

@ -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
}
}

View File

@ -1,12 +0,0 @@
namespace Bloxstrap.Enums.FlagPresets
{
public enum InGameMenuVersion
{
[EnumName(FromTranslation = "Common.Default")]
Default,
V1,
V2,
V4,
V4Chrome
}
}

View File

@ -1,10 +0,0 @@
namespace Bloxstrap.Enums.FlagPresets
{
public enum LightingMode
{
Default,
Voxel,
ShadowMap,
Future
}
}

View File

@ -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
}
}

View File

@ -1,10 +0,0 @@
namespace Bloxstrap.Enums.FlagPresets
{
public enum RenderingMode
{
[EnumName(FromTranslation = "Common.Automatic")]
Default,
D3D11,
D3D10,
}
}

View File

@ -1,12 +0,0 @@
namespace Bloxstrap.Enums.FlagPresets
{
public enum TextureQuality
{
[EnumName(FromTranslation = "Common.Automatic")]
Default,
Level0,
Level1,
Level2,
Level3
}
}

View File

@ -1,9 +0,0 @@
namespace Bloxstrap.Enums
{
public enum GenericTriState
{
Successful,
Failed,
Unknown
}
}

View File

@ -1,10 +0,0 @@
namespace Bloxstrap.Enums
{
public enum LaunchMode
{
None,
Player,
Studio,
StudioAuth
}
}

View File

@ -1,10 +0,0 @@
namespace Bloxstrap.Enums
{
public enum NextAction
{
Terminate,
LaunchSettings,
LaunchRoblox,
LaunchRobloxStudio
}
}

View File

@ -1,9 +0,0 @@
namespace Bloxstrap.Enums
{
public enum ServerType
{
Public,
Private,
Reserved
}
}

View File

@ -1,10 +0,0 @@
namespace Bloxstrap.Enums
{
public enum Theme
{
[EnumName(FromTranslation = "Common.SystemDefault")]
Default,
Light,
Dark
}
}

View File

@ -1,9 +0,0 @@
namespace Bloxstrap.Enums
{
enum VersionComparison
{
LessThan = -1,
Equal = 0,
GreaterThan = 1
}
}

View File

@ -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.")
{
}
}
}

View File

@ -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)
{
}
}
}

View File

@ -1,10 +0,0 @@
namespace Bloxstrap.Exceptions
{
public class InvalidChannelException : Exception
{
public HttpStatusCode? StatusCode;
public InvalidChannelException(HttpStatusCode? statusCode) : base()
=> StatusCode = statusCode;
}
}

View File

@ -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) { }
}
}

View File

@ -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
};
}
}
}

View File

@ -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
};
}
}

View File

@ -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);
}
}
}

View File

@ -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]}";
}
}
}

View File

@ -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);
}
}
}
}

View File

@ -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);
}
}
}
}

View File

@ -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;
}
}
}

View File

@ -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,
_ => "?"
};
}
}

View File

@ -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;
}
}
}

View File

@ -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");
}
}
}

View File

@ -1,7 +0,0 @@
namespace Bloxstrap
{
public static class GlobalCache
{
public static readonly Dictionary<string, string?> ServerLocation = new();
}
}

View File

@ -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;

View File

@ -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;
}
}
}

View File

@ -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
);
}
}
}
}

View File

@ -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);
}
}
}

View File

@ -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);
}
}
}

View File

@ -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!");
}
}
}

View File

@ -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();
});
}
}
}

View File

@ -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}\"";
}
}
}
}

View File

@ -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
}));
}
}
}

View File

@ -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();
}
}
}
}

View File

@ -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}";
}
}

View File

@ -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();
}
}

View File

@ -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();
}
}

View File

@ -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!;
}

View File

@ -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; }
}
}

View File

@ -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!;
}
}

View File

@ -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!;
}
}

View File

@ -1,8 +0,0 @@
namespace Bloxstrap.Models.APIs.Roblox
{
public class ClientFlagSettings
{
[JsonPropertyName("applicationSettings")]
public Dictionary<string, string>? ApplicationSettings { get; set; }
}
}

View File

@ -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; }
}
}

View File

@ -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; }
}
}

View File

@ -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; }
}
}

View File

@ -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!;
}
}

View File

@ -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!;
}
}

View File

@ -1,9 +0,0 @@
namespace Bloxstrap.Models.APIs.Roblox
{
// lmao its just one property
public class UniverseIdResponse
{
[JsonPropertyName("universeId")]
public long UniverseId { get; set; }
}
}

View File

@ -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;
}
}
}

View File

@ -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; }
}
}

View File

@ -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; }
}
}

View File

@ -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; }
}

View File

@ -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; }
}
}

View File

@ -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;
}
}

View File

@ -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();
}
}

View File

@ -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;
}
}

View File

@ -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!;
}
}

View File

@ -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));
}
}
}

View File

@ -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);
}
}
}

View File

@ -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(),
});
}
}
}
}

View File

@ -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;
}
}
}

View File

@ -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!;
}
}

View File

@ -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!;
}
}

View File

@ -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!;
}
}

View File

@ -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;
}
}
}

View File

@ -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);
}
}
}

View File

@ -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}";
}
}
}

View File

@ -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}";
}
}
}

View File

@ -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
});
}
}
}
}

View File

@ -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; }
}
}

View File

@ -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;
}
}

View File

@ -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();
}
}

View File

@ -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; }
}
}

View File

@ -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();
}
}

View File

@ -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) { }
}
}

View File

@ -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) { }
}
}

View File

@ -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) { }
}
}

View File

@ -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;
}
}
}
}

View File

@ -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;
}
}
}

View File

@ -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;
}
}
}

View File

@ -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