diff --git a/.github/ISSUE_TEMPLATE/bug_report.yaml b/.github/ISSUE_TEMPLATE/bug_report.yaml index 00cd191..a52add6 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yaml +++ b/.github/ISSUE_TEMPLATE/bug_report.yaml @@ -45,6 +45,16 @@ body: description: Provide a comprehensive description of the problem you're facing. Don't forget to attach any additional resources you may have, such as log files and screenshots. validations: required: true + - type: textarea + id: repro-steps + attributes: + label: How do you reproduce the problem? + description: Include the steps to reproduce the problem from start to finish. Include details such as FastFlags you added and settings you changed. + placeholder: | + 1. Go to '...' + 2. Click on '...' + 3. Scroll down to '...' + 4. See error - type: textarea id: log attributes: diff --git a/.github/workflows/winget.yml b/.github/workflows/winget.yml index 7e9e09d..e76a433 100644 --- a/.github/workflows/winget.yml +++ b/.github/workflows/winget.yml @@ -8,7 +8,7 @@ jobs: publish: runs-on: ubuntu-latest steps: - - uses: vedantmgoyal2009/winget-releaser@v2 + - uses: vedantmgoyal9/winget-releaser@main with: identifier: pizzaboxer.Bloxstrap token: ${{ secrets.WINGET_TOKEN }} diff --git a/Bloxstrap/App.xaml b/Bloxstrap/App.xaml index ae4bfac..2029b34 100644 --- a/Bloxstrap/App.xaml +++ b/Bloxstrap/App.xaml @@ -11,6 +11,8 @@ + + pack://application:,,,/Resources/Fonts/#Rubik Light diff --git a/Bloxstrap/App.xaml.cs b/Bloxstrap/App.xaml.cs index 5083121..dfc11ca 100644 --- a/Bloxstrap/App.xaml.cs +++ b/Bloxstrap/App.xaml.cs @@ -42,7 +42,7 @@ namespace Bloxstrap public static bool IsProductionBuild => IsActionBuild && BuildMetadata.CommitRef.StartsWith("tag", StringComparison.Ordinal); - public static bool IsStudioVisible => !String.IsNullOrEmpty(App.State.Prop.Studio.VersionGuid); + public static bool IsStudioVisible => !String.IsNullOrEmpty(App.RobloxState.Prop.Studio.VersionGuid); public static readonly MD5 MD5Provider = MD5.Create(); @@ -54,6 +54,8 @@ namespace Bloxstrap public static readonly JsonManager State = new(); + public static readonly JsonManager RobloxState = new(); + public static readonly FastFlagManager FastFlags = new(); public static readonly HttpClient HttpClient = new( @@ -181,6 +183,22 @@ namespace Bloxstrap } } + 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"; @@ -213,6 +231,8 @@ namespace Bloxstrap #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}"); @@ -292,6 +312,7 @@ namespace Bloxstrap { Logger.Initialize(true); Logger.WriteLine(LOG_IDENT, "Not installed, launching the installer"); + AssertWindowsOSVersion(); // prevent new installs from unsupported operating systems LaunchHandler.LaunchInstaller(); } else @@ -317,6 +338,7 @@ namespace Bloxstrap Settings.Load(); State.Load(); + RobloxState.Load(); FastFlags.Load(); if (!Locale.SupportedLocales.ContainsKey(Settings.Prop.Locale)) diff --git a/Bloxstrap/AppData/RobloxPlayerData.cs b/Bloxstrap/AppData/RobloxPlayerData.cs index 3c4f728..bba6cc8 100644 --- a/Bloxstrap/AppData/RobloxPlayerData.cs +++ b/Bloxstrap/AppData/RobloxPlayerData.cs @@ -16,7 +16,7 @@ namespace Bloxstrap.AppData public override string ExecutableName => "RobloxPlayerBeta.exe"; - public override AppState State => App.State.Prop.Player; + public override AppState State => App.RobloxState.Prop.Player; public override IReadOnlyDictionary PackageDirectoryMap { get; set; } = new Dictionary() { diff --git a/Bloxstrap/AppData/RobloxStudioData.cs b/Bloxstrap/AppData/RobloxStudioData.cs index 2ada1c2..18c8e36 100644 --- a/Bloxstrap/AppData/RobloxStudioData.cs +++ b/Bloxstrap/AppData/RobloxStudioData.cs @@ -10,7 +10,7 @@ public override string ExecutableName => "RobloxStudioBeta.exe"; - public override AppState State => App.State.Prop.Studio; + public override AppState State => App.RobloxState.Prop.Studio; public override IReadOnlyDictionary PackageDirectoryMap { get; set; } = new Dictionary() { diff --git a/Bloxstrap/Bloxstrap.csproj b/Bloxstrap/Bloxstrap.csproj index 29525fb..bb30ff6 100644 --- a/Bloxstrap/Bloxstrap.csproj +++ b/Bloxstrap/Bloxstrap.csproj @@ -7,8 +7,8 @@ true True Bloxstrap.ico - 2.8.5 - 2.8.5 + 2.9.0 + 2.9.0 app.manifest true false @@ -25,9 +25,14 @@ + + + + + @@ -49,16 +54,18 @@ - + + - - + + all + diff --git a/Bloxstrap/Bootstrapper.cs b/Bloxstrap/Bootstrapper.cs index ea0cd74..f680e00 100644 --- a/Bloxstrap/Bootstrapper.cs +++ b/Bloxstrap/Bootstrapper.cs @@ -45,10 +45,11 @@ namespace Bloxstrap private readonly FastZipEvents _fastZipEvents = new(); private readonly CancellationTokenSource _cancelTokenSource = new(); - private readonly IAppData AppData; - private readonly LaunchMode _launchMode; + private IAppData AppData = default!; + private LaunchMode _launchMode; private string _launchCommandLine = App.LaunchSettings.RobloxLaunchArgs; + private Version? _latestVersion = null; private string _latestVersionGuid = null!; private string _latestVersionDirectory = null!; private PackageManifest _versionPackageManifest = null!; @@ -58,8 +59,9 @@ namespace Bloxstrap private double _taskbarProgressIncrement; private double _taskbarProgressMaximum; private long _totalDownloadedBytes = 0; + private bool _packageExtractionSuccess = true; - private bool _mustUpgrade => String.IsNullOrEmpty(AppData.State.VersionGuid) || !File.Exists(AppData.ExecutablePath); + private bool _mustUpgrade => App.LaunchSettings.ForceFlag.Active || App.State.Prop.ForceReinstall || String.IsNullOrEmpty(AppData.State.VersionGuid) || !File.Exists(AppData.ExecutablePath); private bool _noConnection = false; private AsyncMutex? _mutex; @@ -69,6 +71,9 @@ namespace Bloxstrap public IBootstrapperDialog? Dialog = null; public bool IsStudioLaunch => _launchMode != LaunchMode.Player; + + public string MutexName { get; set; } = "Bloxstrap-Bootstrapper"; + public bool QuitIfMutexExists { get; set; } = false; #endregion #region Core @@ -78,10 +83,23 @@ namespace Bloxstrap // https://github.com/icsharpcode/SharpZipLib/blob/master/src/ICSharpCode.SharpZipLib/Zip/FastZip.cs/#L669-L680 // exceptions don't get thrown if we define events without actually binding to the failure events. probably a bug. ¯\_(ツ)_/¯ - _fastZipEvents.FileFailure += (_, e) => throw e.Exception; + _fastZipEvents.FileFailure += (_, e) => + { + // only give a pass to font files (no idea whats wrong with them) + if (!e.Name.EndsWith(".ttf")) + throw e.Exception; + + App.Logger.WriteLine("FastZipEvents::OnFileFailure", $"Failed to extract {e.Name}"); + _packageExtractionSuccess = false; + }; _fastZipEvents.DirectoryFailure += (_, e) => throw e.Exception; _fastZipEvents.ProcessFile += (_, e) => e.ContinueRunning = !_cancelTokenSource.IsCancellationRequested; + SetupAppData(); + } + + private void SetupAppData() + { AppData = IsStudioLaunch ? new RobloxStudioData() : new RobloxPlayerData(); Deployment.BinaryType = AppData.BinaryType; } @@ -179,25 +197,29 @@ namespace Bloxstrap } #endif + App.AssertWindowsOSVersion(); + // ensure only one instance of the bootstrapper is running at the time // so that we don't have stuff like two updates happening simultaneously - bool mutexExists = false; + bool mutexExists = Utilities.DoesMutexExist(MutexName); - try + if (mutexExists) { - Mutex.OpenExisting("Bloxstrap-Bootstrapper").Close(); - App.Logger.WriteLine(LOG_IDENT, "Bloxstrap-Bootstrapper mutex exists, waiting..."); - SetStatus(Strings.Bootstrapper_Status_WaitingOtherInstances); - mutexExists = true; - } - catch (Exception) - { - // no mutex exists + if (!QuitIfMutexExists) + { + App.Logger.WriteLine(LOG_IDENT, $"{MutexName} mutex exists, waiting..."); + SetStatus(Strings.Bootstrapper_Status_WaitingOtherInstances); + } + else + { + App.Logger.WriteLine(LOG_IDENT, $"{MutexName} mutex exists, exiting!"); + return; + } } // wait for mutex to be released if it's not yet - await using var mutex = new AsyncMutex(false, "Bloxstrap-Bootstrapper"); + await using var mutex = new AsyncMutex(false, MutexName); await mutex.AcquireAsync(_cancelTokenSource.Token); _mutex = mutex; @@ -207,6 +229,7 @@ namespace Bloxstrap { App.Settings.Load(); App.State.Load(); + App.RobloxState.Load(); } if (!_noConnection) @@ -221,17 +244,42 @@ namespace Bloxstrap } } + CleanupVersionsFolder(); // cleanup after background updater + + bool allModificationsApplied = true; + if (!_noConnection) { if (AppData.State.VersionGuid != _latestVersionGuid || _mustUpgrade) - await UpgradeRoblox(); + { + bool backgroundUpdaterMutexOpen = Utilities.DoesMutexExist("Bloxstrap-BackgroundUpdater"); + if (App.LaunchSettings.BackgroundUpdaterFlag.Active) + backgroundUpdaterMutexOpen = false; // we want to actually update lol + + App.Logger.WriteLine(LOG_IDENT, $"Background updater running: {backgroundUpdaterMutexOpen}"); + + if (backgroundUpdaterMutexOpen && _mustUpgrade) + { + // I am Forced Upgrade, killer of Background Updates + Utilities.KillBackgroundUpdater(); + backgroundUpdaterMutexOpen = false; + } + + if (!backgroundUpdaterMutexOpen) + { + if (IsEligibleForBackgroundUpdate()) + StartBackgroundUpdater(); + else + await UpgradeRoblox(); + } + } if (_cancelTokenSource.IsCancellationRequested) return; // we require deployment details for applying modifications for a worst case scenario, // where we'd need to restore files from a package that isn't present on disk and needs to be redownloaded - await ApplyModifications(); + allModificationsApplied = await ApplyModifications(); } // check registry entries for every launch, just in case the stock bootstrapper changes it back @@ -245,7 +293,15 @@ namespace Bloxstrap await mutex.ReleaseAsync(); if (!App.LaunchSettings.NoLaunchFlag.Active && !_cancelTokenSource.IsCancellationRequested) + { + // show some balloon tips + if (!_packageExtractionSuccess) + Frontend.ShowBalloonTip(Strings.Bootstrapper_ExtractionFailed_Title, Strings.Bootstrapper_ExtractionFailed_Message, ToolTipIcon.Warning); + else if (!allModificationsApplied) + Frontend.ShowBalloonTip(Strings.Bootstrapper_ModificationsFailed_Title, Strings.Bootstrapper_ModificationsFailed_Message, ToolTipIcon.Warning); + StartRoblox(); + } await mutex.ReleaseAsync(); @@ -267,12 +323,17 @@ namespace Bloxstrap using var key = Registry.CurrentUser.CreateSubKey($"SOFTWARE\\ROBLOX Corporation\\Environments\\{AppData.RegistryName}\\Channel"); var match = Regex.Match( - App.LaunchSettings.RobloxLaunchArgs, - "channel:([a-zA-Z0-9-_]+)", + App.LaunchSettings.RobloxLaunchArgs, + "channel:([a-zA-Z0-9-_]+)", RegexOptions.IgnoreCase | RegexOptions.CultureInvariant ); - if (match.Groups.Count == 2) + if (App.LaunchSettings.ChannelFlag.Active && !string.IsNullOrEmpty(App.LaunchSettings.ChannelFlag.Data)) + { + App.Logger.WriteLine(LOG_IDENT, $"Channel set to {App.LaunchSettings.ChannelFlag.Data} from arguments"); + Deployment.Channel = App.LaunchSettings.ChannelFlag.Data.ToLowerInvariant(); + } + else if (match.Groups.Count == 2) { Deployment.Channel = match.Groups[1].Value.ToLowerInvariant(); } @@ -289,37 +350,125 @@ namespace Bloxstrap if (!Deployment.IsDefaultChannel) App.SendStat("robloxChannel", Deployment.Channel); - ClientVersion clientVersion; - - try + if (!App.LaunchSettings.VersionFlag.Active || string.IsNullOrEmpty(App.LaunchSettings.VersionFlag.Data)) { - clientVersion = await Deployment.GetInfo(); + ClientVersion clientVersion; + + try + { + clientVersion = await Deployment.GetInfo(); + } + catch (InvalidChannelException ex) + { + App.Logger.WriteLine(LOG_IDENT, $"Resetting channel from {Deployment.Channel} because {ex.StatusCode}"); + + Deployment.Channel = Deployment.DefaultChannel; + clientVersion = await Deployment.GetInfo(); + } + + key.SetValueSafe("www.roblox.com", Deployment.IsDefaultChannel ? "" : Deployment.Channel); + + _latestVersionGuid = clientVersion.VersionGuid; + _latestVersion = Utilities.ParseVersionSafe(clientVersion.Version); } - catch (InvalidChannelException ex) + else { - App.Logger.WriteLine(LOG_IDENT, $"Resetting channel from {Deployment.Channel} because {ex.StatusCode}"); - - Deployment.Channel = Deployment.DefaultChannel; - clientVersion = await Deployment.GetInfo(); + App.Logger.WriteLine(LOG_IDENT, $"Version set to {App.LaunchSettings.VersionFlag.Data} from arguments"); + _latestVersionGuid = App.LaunchSettings.VersionFlag.Data; + // we can't determine the version } - if (clientVersion.IsBehindDefaultChannel) - { - App.Logger.WriteLine(LOG_IDENT, $"Resetting channel from {Deployment.Channel} because it's behind production"); - - Deployment.Channel = Deployment.DefaultChannel; - clientVersion = await Deployment.GetInfo(); - } - - key.SetValueSafe("www.roblox.com", Deployment.IsDefaultChannel ? "" : Deployment.Channel); - - _latestVersionGuid = clientVersion.VersionGuid; _latestVersionDirectory = Path.Combine(Paths.Versions, _latestVersionGuid); string pkgManifestUrl = Deployment.GetLocation($"/{_latestVersionGuid}-rbxPkgManifest.txt"); var pkgManifestData = await App.HttpClient.GetStringAsync(pkgManifestUrl); _versionPackageManifest = new(pkgManifestData); + + // this can happen if version is set through arguments + if (_launchMode == LaunchMode.Unknown) + { + App.Logger.WriteLine(LOG_IDENT, "Identifying launch mode from package manifest"); + + bool isPlayer = _versionPackageManifest.Exists(x => x.Name == "RobloxApp.zip"); + App.Logger.WriteLine(LOG_IDENT, $"isPlayer: {isPlayer}"); + + _launchMode = isPlayer ? LaunchMode.Player : LaunchMode.Studio; + SetupAppData(); // we need to set it up again + } + } + + private bool IsEligibleForBackgroundUpdate() + { + const string LOG_IDENT = "Bootstrapper::IsEligibleForBackgroundUpdate"; + + if (App.LaunchSettings.BackgroundUpdaterFlag.Active) + { + App.Logger.WriteLine(LOG_IDENT, "Not eligible: Is the background updater process"); + return false; + } + + if (!App.Settings.Prop.BackgroundUpdatesEnabled) + { + App.Logger.WriteLine(LOG_IDENT, "Not eligible: Background updates disabled"); + return false; + } + + if (IsStudioLaunch) + { + App.Logger.WriteLine(LOG_IDENT, "Not eligible: Studio launch"); + return false; + } + + if (_mustUpgrade) + { + App.Logger.WriteLine(LOG_IDENT, "Not eligible: Must upgrade is true"); + return false; + } + + // at least 3GB of free space + const long minimumFreeSpace = 3_000_000_000; + long space = Filesystem.GetFreeDiskSpace(Paths.Base); + if (space < minimumFreeSpace) + { + App.Logger.WriteLine(LOG_IDENT, $"Not eligible: User has {space} free space, at least {minimumFreeSpace} is required"); + return false; + } + + if (_latestVersion == default) + { + App.Logger.WriteLine(LOG_IDENT, "Not eligible: Latest version is undefined"); + return false; + } + + Version? currentVersion = Utilities.GetRobloxVersion(AppData); + if (currentVersion == default) + { + App.Logger.WriteLine(LOG_IDENT, "Not eligible: Current version is undefined"); + return false; + } + + // always normally upgrade for downgrades + if (currentVersion.Minor > _latestVersion.Minor) + { + App.Logger.WriteLine(LOG_IDENT, "Not eligible: Downgrade"); + return false; + } + + // only background update if we're: + // - one major update behind + // - the same major update + int diff = _latestVersion.Minor - currentVersion.Minor; + if (diff == 0 || diff == 1) + { + App.Logger.WriteLine(LOG_IDENT, "Eligible"); + return true; + } + else + { + App.Logger.WriteLine(LOG_IDENT, $"Not eligible: Major version diff is {diff}"); + return false; + } } private void StartRoblox() @@ -649,16 +798,51 @@ namespace Bloxstrap #endregion #region Roblox Install - private void CleanupVersionsFolder() + private static bool TryDeleteRobloxInDirectory(string dir) + { + string clientPath = Path.Combine(dir, "RobloxPlayerBeta.exe"); + if (!File.Exists(dir)) + { + clientPath = Path.Combine(dir, "RobloxStudioBeta.exe"); + if (!File.Exists(dir)) + return true; // ok??? + } + + try + { + File.Delete(clientPath); + return true; + } + catch (Exception) + { + return false; + } + } + + public static void CleanupVersionsFolder() { const string LOG_IDENT = "Bootstrapper::CleanupVersionsFolder"; + if (App.LaunchSettings.BackgroundUpdaterFlag.Active) + { + App.Logger.WriteLine(LOG_IDENT, "Background updater tried to cleanup, stopping!"); + return; + } + foreach (string dir in Directory.GetDirectories(Paths.Versions)) { string dirName = Path.GetFileName(dir); - if (dirName != App.State.Prop.Player.VersionGuid && dirName != App.State.Prop.Studio.VersionGuid) + if (dirName != App.RobloxState.Prop.Player.VersionGuid && dirName != App.RobloxState.Prop.Studio.VersionGuid) { + // TODO: this is too expensive + //Filesystem.AssertReadOnlyDirectory(dir); + + // check if it's still being used first + // we dont want to accidentally delete the files of a running roblox instance + if (!TryDeleteRobloxInDirectory(dir)) + continue; + try { Directory.Delete(dir, true); @@ -729,11 +913,11 @@ namespace Bloxstrap _isInstalling = true; // make sure nothing is running before continuing upgrade - if (!IsStudioLaunch) // TODO: wait for studio processes to close before updating to prevent data loss + if (!App.LaunchSettings.BackgroundUpdaterFlag.Active && !IsStudioLaunch) // TODO: wait for studio processes to close before updating to prevent data loss KillRobloxPlayers(); // get a fully clean install - if (Directory.Exists(_latestVersionDirectory)) + if (!App.LaunchSettings.BackgroundUpdaterFlag.Active && Directory.Exists(_latestVersionDirectory)) { try { @@ -886,23 +1070,26 @@ namespace Bloxstrap var allPackageHashes = new List(); - allPackageHashes.AddRange(App.State.Prop.Player.PackageHashes.Values); - allPackageHashes.AddRange(App.State.Prop.Studio.PackageHashes.Values); + allPackageHashes.AddRange(App.RobloxState.Prop.Player.PackageHashes.Values); + allPackageHashes.AddRange(App.RobloxState.Prop.Studio.PackageHashes.Values); - foreach (string hash in cachedPackageHashes) + if (!App.Settings.Prop.DebugDisableVersionPackageCleanup) { - if (!allPackageHashes.Contains(hash)) + foreach (string hash in cachedPackageHashes) { - App.Logger.WriteLine(LOG_IDENT, $"Deleting unused package {hash}"); - - try + if (!allPackageHashes.Contains(hash)) { - File.Delete(Path.Combine(Paths.Downloads, hash)); - } - catch (Exception ex) - { - App.Logger.WriteLine(LOG_IDENT, $"Failed to delete {hash}!"); - App.Logger.WriteException(LOG_IDENT, ex); + App.Logger.WriteLine(LOG_IDENT, $"Deleting unused package {hash}"); + + try + { + File.Delete(Path.Combine(Paths.Downloads, hash)); + } + catch (Exception ex) + { + App.Logger.WriteLine(LOG_IDENT, $"Failed to delete {hash}!"); + App.Logger.WriteException(LOG_IDENT, ex); + } } } } @@ -913,7 +1100,7 @@ namespace Bloxstrap AppData.State.Size = distributionSize; - int totalSize = App.State.Prop.Player.Size + App.State.Prop.Studio.Size; + int totalSize = App.RobloxState.Prop.Player.Size + App.RobloxState.Prop.Studio.Size; using (var uninstallKey = Registry.CurrentUser.CreateSubKey(App.UninstallKey)) { @@ -923,14 +1110,32 @@ namespace Bloxstrap App.Logger.WriteLine(LOG_IDENT, $"Registered as {totalSize} KB"); App.State.Save(); + App.RobloxState.Save(); _isInstalling = false; } - private async Task ApplyModifications() + private static void StartBackgroundUpdater() + { + const string LOG_IDENT = "Bootstrapper::StartBackgroundUpdater"; + + if (Utilities.DoesMutexExist("Bloxstrap-BackgroundUpdater")) + { + App.Logger.WriteLine(LOG_IDENT, "Background updater already running"); + return; + } + + App.Logger.WriteLine(LOG_IDENT, "Starting background updater"); + + Process.Start(Paths.Process, "-backgroundupdater"); + } + + private async Task ApplyModifications() { const string LOG_IDENT = "Bootstrapper::ApplyModifications"; + bool success = true; + SetStatus(Strings.Bootstrapper_Status_ApplyingModifications); // handle file mods @@ -1006,7 +1211,7 @@ namespace Bloxstrap foreach (string file in Directory.GetFiles(Paths.Modifications, "*.*", SearchOption.AllDirectories)) { if (_cancelTokenSource.IsCancellationRequested) - return; + return true; // get relative directory path string relativeFile = file.Substring(Paths.Modifications.Length + 1); @@ -1038,10 +1243,18 @@ namespace Bloxstrap Directory.CreateDirectory(Path.GetDirectoryName(fileVersionFolder)!); Filesystem.AssertReadOnly(fileVersionFolder); - File.Copy(fileModFolder, fileVersionFolder, true); - Filesystem.AssertReadOnly(fileVersionFolder); - - App.Logger.WriteLine(LOG_IDENT, $"{relativeFile} has been copied to the version folder"); + try + { + File.Copy(fileModFolder, fileVersionFolder, true); + Filesystem.AssertReadOnly(fileVersionFolder); + App.Logger.WriteLine(LOG_IDENT, $"{relativeFile} has been copied to the version folder"); + } + catch (Exception ex) + { + App.Logger.WriteLine(LOG_IDENT, $"Failed to apply modification ({relativeFile})"); + App.Logger.WriteException(LOG_IDENT, ex); + success = false; + } } // the manifest is primarily here to keep track of what files have been @@ -1050,7 +1263,7 @@ namespace Bloxstrap var fileRestoreMap = new Dictionary>(); - foreach (string fileLocation in App.State.Prop.ModManifest) + foreach (string fileLocation in App.RobloxState.Prop.ModManifest) { if (modFolderFiles.Contains(fileLocation)) continue; @@ -1088,17 +1301,31 @@ namespace Bloxstrap if (package is not null) { if (_cancelTokenSource.IsCancellationRequested) - return; + return true; await DownloadPackage(package); ExtractPackage(package, entry.Value); } } - App.State.Prop.ModManifest = modFolderFiles; - App.State.Save(); + // make sure we're not overwriting a new update + // if we're the background update process, always overwrite + if (App.LaunchSettings.BackgroundUpdaterFlag.Active || !App.RobloxState.HasFileOnDiskChanged()) + { + App.RobloxState.Prop.ModManifest = modFolderFiles; + App.RobloxState.Save(); + } + else + { + App.Logger.WriteLine(LOG_IDENT, "RobloxState disk mismatch, not saving ModManifest"); + } App.Logger.WriteLine(LOG_IDENT, $"Finished checking file mods"); + + if (!success) + App.Logger.WriteLine(LOG_IDENT, "Failed to apply all modifications"); + + return success; } private async Task DownloadPackage(Package package) diff --git a/Bloxstrap/Enums/BootstrapperStyle.cs b/Bloxstrap/Enums/BootstrapperStyle.cs index 5c5f6fd..c638858 100644 --- a/Bloxstrap/Enums/BootstrapperStyle.cs +++ b/Bloxstrap/Enums/BootstrapperStyle.cs @@ -10,6 +10,7 @@ ByfronDialog, [EnumName(StaticName = "Bloxstrap")] FluentDialog, - FluentAeroDialog + FluentAeroDialog, + CustomDialog } } diff --git a/Bloxstrap/Enums/CustomThemeTemplate.cs b/Bloxstrap/Enums/CustomThemeTemplate.cs new file mode 100644 index 0000000..6497907 --- /dev/null +++ b/Bloxstrap/Enums/CustomThemeTemplate.cs @@ -0,0 +1,8 @@ +namespace Bloxstrap.Enums +{ + public enum CustomThemeTemplate + { + Blank, + Simple + } +} diff --git a/Bloxstrap/Enums/LaunchMode.cs b/Bloxstrap/Enums/LaunchMode.cs index c045d4f..4778442 100644 --- a/Bloxstrap/Enums/LaunchMode.cs +++ b/Bloxstrap/Enums/LaunchMode.cs @@ -3,6 +3,10 @@ public enum LaunchMode { None, + /// + /// Launch mode will be determined inside the bootstrapper. Only works if the VersionFlag is set. + /// + Unknown, Player, Studio, StudioAuth diff --git a/Bloxstrap/Exceptions/CustomThemeException.cs b/Bloxstrap/Exceptions/CustomThemeException.cs new file mode 100644 index 0000000..68ad0a1 --- /dev/null +++ b/Bloxstrap/Exceptions/CustomThemeException.cs @@ -0,0 +1,60 @@ +using Bloxstrap.Extensions; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Bloxstrap.Exceptions +{ + internal class CustomThemeException : Exception + { + /// + /// The exception message in English (for logging) + /// + public string EnglishMessage { get; } = null!; + + public CustomThemeException(string translationString) + : base(Strings.ResourceManager.GetStringSafe(translationString)) + { + EnglishMessage = Strings.ResourceManager.GetStringSafe(translationString, new CultureInfo("en-GB")); + } + + public CustomThemeException(Exception innerException, string translationString) + : base(Strings.ResourceManager.GetStringSafe(translationString), innerException) + { + EnglishMessage = Strings.ResourceManager.GetStringSafe(translationString, new CultureInfo("en-GB")); + } + + public CustomThemeException(string translationString, params object?[] args) + : base(string.Format(Strings.ResourceManager.GetStringSafe(translationString), args)) + { + EnglishMessage = string.Format(Strings.ResourceManager.GetStringSafe(translationString, new CultureInfo("en-GB")), args); + } + + public CustomThemeException(Exception innerException, string translationString, params object?[] args) + : base(string.Format(Strings.ResourceManager.GetStringSafe(translationString), args), innerException) + { + EnglishMessage = string.Format(Strings.ResourceManager.GetStringSafe(translationString, new CultureInfo("en-GB")), args); + } + + public override string ToString() + { + StringBuilder sb = new StringBuilder(GetType().ToString()); + + if (!string.IsNullOrEmpty(Message)) + sb.Append($": {Message}"); + + if (!string.IsNullOrEmpty(EnglishMessage) && Message != EnglishMessage) + sb.Append($" ({EnglishMessage})"); + + if (InnerException != null) + sb.Append($"\r\n ---> {InnerException}\r\n "); + + if (StackTrace != null) + sb.Append($"\r\n{StackTrace}"); + + return sb.ToString(); + } + } +} diff --git a/Bloxstrap/Extensions/BootstrapperStyleEx.cs b/Bloxstrap/Extensions/BootstrapperStyleEx.cs index 3802264..693c78e 100644 --- a/Bloxstrap/Extensions/BootstrapperStyleEx.cs +++ b/Bloxstrap/Extensions/BootstrapperStyleEx.cs @@ -13,7 +13,8 @@ BootstrapperStyle.ProgressDialog, BootstrapperStyle.LegacyDialog2011, BootstrapperStyle.LegacyDialog2008, - BootstrapperStyle.VistaDialog + BootstrapperStyle.VistaDialog, + BootstrapperStyle.CustomDialog }; } } diff --git a/Bloxstrap/Extensions/CustomThemeTemplateEx.cs b/Bloxstrap/Extensions/CustomThemeTemplateEx.cs new file mode 100644 index 0000000..ed9dcea --- /dev/null +++ b/Bloxstrap/Extensions/CustomThemeTemplateEx.cs @@ -0,0 +1,36 @@ +using System.Text; + +namespace Bloxstrap.Extensions +{ + static class CustomThemeTemplateEx + { + const string EXAMPLES_URL = "https://github.com/bloxstraplabs/custom-bootstrapper-examples"; + + public static string GetFileName(this CustomThemeTemplate template) + { + return $"CustomBootstrapperTemplate_{template}.xml"; + } + + public static string GetFileContents(this CustomThemeTemplate template) + { + string contents = Encoding.UTF8.GetString(Resource.Get(template.GetFileName()).Result); + + switch (template) + { + case CustomThemeTemplate.Blank: + { + string moreText = string.Format(Strings.CustomTheme_Templates_Blank_MoreExamples, EXAMPLES_URL); + return string.Format(contents, Strings.CustomTheme_Templates_Blank_UIElements, moreText); + } + case CustomThemeTemplate.Simple: + { + string moreText = string.Format(Strings.CustomTheme_Templates_Simple_MoreExamples, EXAMPLES_URL); + return string.Format(contents, moreText); + } + default: + Debug.Assert(false); + return contents; + } + } + } +} diff --git a/Bloxstrap/Installer.cs b/Bloxstrap/Installer.cs index 93f40c1..d57e985 100644 --- a/Bloxstrap/Installer.cs +++ b/Bloxstrap/Installer.cs @@ -197,7 +197,7 @@ namespace Bloxstrap var processes = new List(); - if (!String.IsNullOrEmpty(App.State.Prop.Player.VersionGuid)) + if (!String.IsNullOrEmpty(App.RobloxState.Prop.Player.VersionGuid)) processes.AddRange(Process.GetProcessesByName(App.RobloxPlayerAppName)); if (App.IsStudioVisible) @@ -587,16 +587,23 @@ namespace Bloxstrap } } - if (Utilities.CompareVersions(existingVer, "2.8.3") == VersionComparison.LessThan) + if (Utilities.CompareVersions(existingVer, "2.9.0") == VersionComparison.LessThan) { - // force reinstallation - App.State.Prop.Player.VersionGuid = ""; - App.State.Prop.Studio.VersionGuid = ""; + // move from App.State to App.RobloxState + if (App.State.Prop.GetDeprecatedPlayer() != null) + App.RobloxState.Prop.Player = App.State.Prop.GetDeprecatedPlayer()!; + + if (App.State.Prop.GetDeprecatedStudio() != null) + App.RobloxState.Prop.Studio = App.State.Prop.GetDeprecatedStudio()!; + + if (App.State.Prop.GetDeprecatedModManifest() != null) + App.RobloxState.Prop.ModManifest = App.State.Prop.GetDeprecatedModManifest()!; } App.Settings.Save(); App.FastFlags.Save(); App.State.Save(); + App.RobloxState.Save(); } if (currentVer is null) diff --git a/Bloxstrap/JsonManager.cs b/Bloxstrap/JsonManager.cs index bb1eb77..6c04a2a 100644 --- a/Bloxstrap/JsonManager.cs +++ b/Bloxstrap/JsonManager.cs @@ -8,6 +8,11 @@ namespace Bloxstrap public T Prop { get; set; } = new(); + /// + /// The file hash when last retrieved from disk + /// + public string? LastFileHash { get; private set; } + public virtual string ClassName => typeof(T).Name; public virtual string FileLocation => Path.Combine(Paths.Base, $"{ClassName}.json"); @@ -22,12 +27,15 @@ namespace Bloxstrap try { - T? settings = JsonSerializer.Deserialize(File.ReadAllText(FileLocation)); + string contents = File.ReadAllText(FileLocation); + + T? settings = JsonSerializer.Deserialize(contents); if (settings is null) throw new ArgumentNullException("Deserialization returned null"); Prop = settings; + LastFileHash = MD5Hash.FromString(contents); App.Logger.WriteLine(LOG_IDENT, "Loaded successfully!"); } @@ -74,7 +82,11 @@ namespace Bloxstrap try { - File.WriteAllText(FileLocation, JsonSerializer.Serialize(Prop, new JsonSerializerOptions { WriteIndented = true })); + string contents = JsonSerializer.Serialize(Prop, new JsonSerializerOptions { WriteIndented = true }); + + File.WriteAllText(FileLocation, contents); + + LastFileHash = MD5Hash.FromString(contents); } catch (Exception ex) when (ex is IOException or UnauthorizedAccessException) { @@ -89,5 +101,13 @@ namespace Bloxstrap App.Logger.WriteLine(LOG_IDENT, "Save complete!"); } + + /// + /// Is the file on disk different to the one deserialised during this session? + /// + public bool HasFileOnDiskChanged() + { + return LastFileHash != MD5Hash.FromFile(FileLocation); + } } } diff --git a/Bloxstrap/LaunchHandler.cs b/Bloxstrap/LaunchHandler.cs index 2dda0c9..1eee8b2 100644 --- a/Bloxstrap/LaunchHandler.cs +++ b/Bloxstrap/LaunchHandler.cs @@ -4,6 +4,7 @@ using Windows.Win32; using Windows.Win32.Foundation; using Bloxstrap.UI.Elements.Dialogs; +using Bloxstrap.Enums; namespace Bloxstrap { @@ -58,6 +59,11 @@ namespace Bloxstrap App.Logger.WriteLine(LOG_IDENT, "Opening watcher"); LaunchWatcher(); } + else if (App.LaunchSettings.BackgroundUpdaterFlag.Active) + { + App.Logger.WriteLine(LOG_IDENT, "Opening background updater"); + LaunchBackgroundUpdater(); + } else if (App.LaunchSettings.RobloxLaunchMode != LaunchMode.None) { App.Logger.WriteLine(LOG_IDENT, $"Opening bootstrapper ({App.LaunchSettings.RobloxLaunchMode})"); @@ -295,5 +301,51 @@ namespace Bloxstrap App.Terminate(); }); } + + public static void LaunchBackgroundUpdater() + { + const string LOG_IDENT = "LaunchHandler::LaunchBackgroundUpdater"; + + // Activate some LaunchFlags we need + App.LaunchSettings.QuietFlag.Active = true; + App.LaunchSettings.NoLaunchFlag.Active = true; + + App.Logger.WriteLine(LOG_IDENT, "Initializing bootstrapper"); + App.Bootstrapper = new Bootstrapper(LaunchMode.Player) + { + MutexName = "Bloxstrap-BackgroundUpdater", + QuitIfMutexExists = true + }; + + CancellationTokenSource cts = new CancellationTokenSource(); + + Task.Run(() => + { + App.Logger.WriteLine(LOG_IDENT, "Started event waiter"); + using (EventWaitHandle handle = new EventWaitHandle(false, EventResetMode.AutoReset, "Bloxstrap-BackgroundUpdaterKillEvent")) + handle.WaitOne(); + + App.Logger.WriteLine(LOG_IDENT, "Received close event, killing it all!"); + App.Bootstrapper.Cancel(); + }, cts.Token); + + Task.Run(App.Bootstrapper.Run).ContinueWith(t => + { + App.Logger.WriteLine(LOG_IDENT, "Bootstrapper task has finished"); + cts.Cancel(); // stop event waiter + + 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(); + }); + + App.Logger.WriteLine(LOG_IDENT, "Exiting"); + } } } diff --git a/Bloxstrap/LaunchSettings.cs b/Bloxstrap/LaunchSettings.cs index f2ae908..e0a5051 100644 --- a/Bloxstrap/LaunchSettings.cs +++ b/Bloxstrap/LaunchSettings.cs @@ -12,25 +12,33 @@ namespace Bloxstrap { public class LaunchSettings { - public LaunchFlag MenuFlag { get; } = new("preferences,menu,settings"); + public LaunchFlag MenuFlag { get; } = new("preferences,menu,settings"); - public LaunchFlag WatcherFlag { get; } = new("watcher"); + public LaunchFlag WatcherFlag { get; } = new("watcher"); - public LaunchFlag QuietFlag { get; } = new("quiet"); + public LaunchFlag BackgroundUpdaterFlag { get; } = new("backgroundupdater"); - public LaunchFlag UninstallFlag { get; } = new("uninstall"); + public LaunchFlag QuietFlag { get; } = new("quiet"); - public LaunchFlag NoLaunchFlag { get; } = new("nolaunch"); + public LaunchFlag UninstallFlag { get; } = new("uninstall"); + + public LaunchFlag NoLaunchFlag { get; } = new("nolaunch"); - public LaunchFlag TestModeFlag { get; } = new("testmode"); + public LaunchFlag TestModeFlag { get; } = new("testmode"); - public LaunchFlag NoGPUFlag { get; } = new("nogpu"); + public LaunchFlag NoGPUFlag { get; } = new("nogpu"); - public LaunchFlag UpgradeFlag { get; } = new("upgrade"); + public LaunchFlag UpgradeFlag { get; } = new("upgrade"); - public LaunchFlag PlayerFlag { get; } = new("player"); + public LaunchFlag PlayerFlag { get; } = new("player"); - public LaunchFlag StudioFlag { get; } = new("studio"); + public LaunchFlag StudioFlag { get; } = new("studio"); + + public LaunchFlag VersionFlag { get; } = new("version"); + + public LaunchFlag ChannelFlag { get; } = new("channel"); + + public LaunchFlag ForceFlag { get; } = new("force"); #if DEBUG public bool BypassUpdateCheck => true; @@ -87,6 +95,13 @@ namespace Bloxstrap RobloxLaunchArgs = arg; startIdx = 1; } + else if (arg.StartsWith("version-")) + { + App.Logger.WriteLine(LOG_IDENT, "Got version argument"); + VersionFlag.Active = true; + VersionFlag.Data = arg; + startIdx = 1; + } } // parse @@ -108,11 +123,18 @@ namespace Bloxstrap continue; } + if (flag.Active) + { + App.Logger.WriteLine(LOG_IDENT, $"Tried to set {identifier} flag twice"); + 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 @@ -121,6 +143,9 @@ namespace Bloxstrap } } + if (VersionFlag.Active) + RobloxLaunchMode = LaunchMode.Unknown; // determine in bootstrapper + if (PlayerFlag.Active) ParsePlayer(PlayerFlag.Data); else if (StudioFlag.Active) diff --git a/Bloxstrap/Models/APIs/Roblox/ClientVersion.cs b/Bloxstrap/Models/APIs/Roblox/ClientVersion.cs index 9fa405e..5e584f4 100644 --- a/Bloxstrap/Models/APIs/Roblox/ClientVersion.cs +++ b/Bloxstrap/Models/APIs/Roblox/ClientVersion.cs @@ -12,7 +12,5 @@ public string BootstrapperVersion { get; set; } = null!; public DateTime? Timestamp { get; set; } - - public bool IsBehindDefaultChannel { get; set; } } } diff --git a/Bloxstrap/Models/Persistable/RobloxState.cs b/Bloxstrap/Models/Persistable/RobloxState.cs new file mode 100644 index 0000000..f4b3098 --- /dev/null +++ b/Bloxstrap/Models/Persistable/RobloxState.cs @@ -0,0 +1,11 @@ +namespace Bloxstrap.Models.Persistable +{ + public class RobloxState + { + public AppState Player { get; set; } = new(); + + public AppState Studio { get; set; } = new(); + + public List ModManifest { get; set; } = new(); + } +} diff --git a/Bloxstrap/Models/Persistable/Settings.cs b/Bloxstrap/Models/Persistable/Settings.cs index d640fc3..0f6bb86 100644 --- a/Bloxstrap/Models/Persistable/Settings.cs +++ b/Bloxstrap/Models/Persistable/Settings.cs @@ -17,6 +17,9 @@ namespace Bloxstrap.Models.Persistable public bool UseFastFlagManager { get; set; } = true; public bool WPFSoftwareRender { get; set; } = false; public bool EnableAnalytics { get; set; } = true; + public bool BackgroundUpdatesEnabled { get; set; } = true; + public bool DebugDisableVersionPackageCleanup { get; set; } = false; + public string? SelectedCustomTheme { get; set; } = null; // integration configuration public bool EnableActivityTracking { get; set; } = true; diff --git a/Bloxstrap/Models/Persistable/State.cs b/Bloxstrap/Models/Persistable/State.cs index de05265..070f8e0 100644 --- a/Bloxstrap/Models/Persistable/State.cs +++ b/Bloxstrap/Models/Persistable/State.cs @@ -6,12 +6,28 @@ public bool PromptWebView2Install { get; set; } = true; - public AppState Player { get; set; } = new(); - - public AppState Studio { get; set; } = new(); + public bool ForceReinstall { get; set; } = false; public WindowState SettingsWindow { get; set; } = new(); - public List ModManifest { get; set; } = new(); + #region Deprecated properties + /// + /// Deprecated, use App.RobloxState.Player + /// + public AppState? Player { private get; set; } + public AppState? GetDeprecatedPlayer() => Player; + + /// + /// Deprecated, use App.RobloxState.Studio + /// + public AppState? Studio { private get; set; } + public AppState? GetDeprecatedStudio() => Studio; + + /// + /// Deprecated, use App.RobloxState.ModManifest + /// + public List? ModManifest { private get; set; } + public List? GetDeprecatedModManifest() => ModManifest; + #endregion } } diff --git a/Bloxstrap/Paths.cs b/Bloxstrap/Paths.cs index 43d4a1c..1205d67 100644 --- a/Bloxstrap/Paths.cs +++ b/Bloxstrap/Paths.cs @@ -22,6 +22,7 @@ public static string Integrations { get; private set; } = ""; public static string Versions { get; private set; } = ""; public static string Modifications { get; private set; } = ""; + public static string CustomThemes { get; private set; } = ""; public static string Application { get; private set; } = ""; @@ -37,6 +38,7 @@ Integrations = Path.Combine(Base, "Integrations"); Versions = Path.Combine(Base, "Versions"); Modifications = Path.Combine(Base, "Modifications"); + CustomThemes = Path.Combine(Base, "CustomThemes"); Application = Path.Combine(Base, $"{App.ProjectName}.exe"); } diff --git a/Bloxstrap/Resource.cs b/Bloxstrap/Resource.cs index 46c8e82..612d846 100644 --- a/Bloxstrap/Resource.cs +++ b/Bloxstrap/Resource.cs @@ -21,5 +21,10 @@ namespace Bloxstrap await stream.CopyToAsync(memoryStream); return memoryStream.ToArray(); } + + public static async Task GetString(string name) + { + return Encoding.UTF8.GetString(await Get(name)); + } } } diff --git a/Bloxstrap/Resources/CustomBootstrapperSchema.json b/Bloxstrap/Resources/CustomBootstrapperSchema.json new file mode 100644 index 0000000..44b6631 --- /dev/null +++ b/Bloxstrap/Resources/CustomBootstrapperSchema.json @@ -0,0 +1,521 @@ +{ + "Elements": { + "FrameworkElement": { + "IsCreatable": false, + "Attributes": { + "Name": "string", + "Visibility": "Visibility", + "IsEnabled": "bool", + "Margin": "Thickness", + "Height": "double", + "Width": "double", + "HorizontalAlignment": "HorizontalAlignment", + "VerticalAlignment": "VerticalAlignment", + "RenderTransform": "Transform", + "LayoutTransform": "Transform", + "Opacity": "double", + "OpacityMask": "Brush", + "RenderTransformOrigin": "Point", + "Panel.ZIndex": "int", + "Grid.Row": "int", + "Grid.RowSpan": "int", + "Grid.Column": "int", + "Grid.ColumnSpan": "int" + } + }, + "Control": { + "SuperClass": "FrameworkElement", + "IsCreatable": false, + "Attributes": { + "Padding": "Thickness", + "BorderThickness": "Thickness", + "Foreground": "Brush", + "Background": "Brush", + "BorderBrush": "Brush", + "FontSize": "double", + "FontWeight": "FontWeight", + "FontStyle": "FontStyle", + "FontFamily": "FontFamily" + } + }, + "BloxstrapCustomBootstrapper": { + "SuperClass": "Control", + "IsCreatable": true, + "Attributes": { + "Version": "int", + "Theme": "Theme", + "Title": "string", + "IgnoreTitleBarInset": "bool", + "WindowCornerPreference": "WindowCornerPreference" + } + }, + "TitleBar": { + "SuperClass": "Control", + "IsCreatable": true, + "Attributes": { + "ShowMinimize": "bool", + "ShowClose": "bool", + "Title": "string" + } + }, + "Button": { + "SuperClass": "Control", + "IsCreatable": true, + "Attributes": { + "Content": "object" + } + }, + "RangeBase": { + "SuperClass": "Control", + "IsCreatable": false, + "Attributes": { + "Value": "double", + "Maximum": "double" + } + }, + "ProgressBar": { + "SuperClass": "RangeBase", + "IsCreatable": true, + "Attributes": { + "IsIndeterminate": "bool", + "CornerRadius": "CornerRadius", + "IndicatorCornerRadius": "CornerRadius" + } + }, + "ProgressRing": { + "SuperClass": "RangeBase", + "IsCreatable": true, + "Attributes": { + "IsIndeterminate": "bool" + } + }, + "TextBlock": { + "SuperClass": "FrameworkElement", + "IsCreatable": true, + "Attributes": { + "Text": "string", + "Foreground": "Brush", + "Background": "Brush", + "FontSize": "double", + "FontWeight": "FontWeight", + "FontStyle": "FontStyle", + "FontFamily": "FontFamily", + "LineHeight": "double", + "LineStackingStrategy": "LineStackingStrategy", + "TextAlignment": "TextAlignment", + "TextTrimming": "TextTrimming", + "TextWrapping": "TextWrapping", + "TextDecorations": "TextDecorations", + "IsHyphenationEnabled": "bool", + "BaselineOffset": "double", + "Padding": "Thickness" + } + }, + "MarkdownTextBlock": { + "SuperClass": "TextBlock", + "IsCreatable": true, + "Attributes": {} + }, + "Image": { + "SuperClass": "FrameworkElement", + "IsCreatable": true, + "Attributes": { + "Stretch": "Stretch", + "StretchDirection": "StretchDirection", + "Source": "ImageSource", + "IsAnimated": "bool" + } + }, + "Grid": { + "SuperClass": "FrameworkElement", + "IsCreatable": true, + "Attributes": { + "RowDefinitions": "object", + "ColumnDefinitions": "object" + } + }, + "StackPanel": { + "SuperClass": "FrameworkElement", + "IsCreatable": true, + "Attributes": { + "Orientation": "Orientation" + } + }, + "Border": { + "SuperClass": "FrameworkElement", + "IsCreatable": true, + "Attributes": { + "Background": "Brush", + "BorderBrush": "Brush", + "BorderThickness": "Thickness", + "Padding": "Thickness", + "CornerRadius": "CornerRadius" + } + }, + "RowDefinition": { + "IsCreatable": true, + "Attributes": { + "Height": "GridLength", + "MinHeight": "double", + "MaxHeight": "double" + } + }, + "ColumnDefinition": { + "IsCreatable": true, + "Attributes": { + "Width": "GridLength", + "MinWidth": "double", + "MaxWidth": "double" + } + }, + "ScaleTransform": { + "IsCreatable": true, + "Attributes": { + "ScaleX": "double", + "ScaleY": "double", + "CenterX": "double", + "CenterY": "double" + } + }, + "SkewTransform": { + "IsCreatable": true, + "Attributes": { + "AngleX": "double", + "AngleY": "double", + "CenterX": "double", + "CenterY": "double" + } + }, + "RotateTransform": { + "IsCreatable": true, + "Attributes": { + "Angle": "double", + "CenterX": "double", + "CenterY": "double" + } + }, + "TranslateTransform": { + "IsCreatable": true, + "Attributes": { + "X": "double", + "Y": "double" + } + }, + "Brush": { + "IsCreatable": false, + "Attributes": { + "Opacity": "double" + } + }, + "SolidColorBrush": { + "SuperClass": "Brush", + "IsCreatable": true, + "Attributes": { + "Color": "Color" + } + }, + "ImageBrush": { + "SuperClass": "Brush", + "IsCreatable": true, + "Attributes": { + "AlignmentX": "AlignmentX", + "AlignmentY": "AlignmentY", + "Stretch": "Stretch", + "TileMode": "TileMode", + "ViewboxUnits": "BrushMappingMode", + "ViewportUnits": "BrushMappingMode", + "Viewbox": "Rect", + "Viewport": "Rect", + "ImageSource": "ImageSource" + } + }, + "LinearGradientBrush": { + "SuperClass": "Brush", + "IsCreatable": true, + "Attributes": { + "StartPoint": "Point", + "EndPoint": "Point", + "ColorInterpolationMode": "ColorInterpolationMode", + "MappingMode": "BrushMappingMode", + "SpreadMethod": "GradientSpreadMethod" + } + }, + "GradientStop": { + "IsCreatable": true, + "Attributes": { + "Color": "Color", + "Offset": "double" + } + }, + "Shape": { + "SuperClass": "FrameworkElement", + "IsCreatable": false, + "Attributes": { + "Fill": "Brush", + "Stroke": "Brush", + "Stretch": "Stretch", + "StrokeDashCap": "PenLineCap", + "StrokeDashOffset": "double", + "StrokeEndLineCap": "PenLineCap", + "StrokeLineJoin": "PenLineJoin", + "StrokeMiterLimit": "double", + "StrokeStartLineCap": "PenLineCap", + "StrokeThickness": "double" + } + }, + "Ellipse": { + "SuperClass": "Shape", + "IsCreatable": true, + "Attributes": {} + }, + "Line": { + "SuperClass": "Shape", + "IsCreatable": true, + "Attributes": { + "X1": "double", + "X2": "double", + "Y1": "double", + "Y2": "double" + } + }, + "Rectangle": { + "SuperClass": "Shape", + "IsCreatable": true, + "Attributes": { + "RadiusX": "double", + "RadiusY": "double" + } + }, + "BlurEffect": { + "IsCreatable": true, + "Attributes": { + "KernelType": "KernelType", + "Radius": "double", + "RenderingBias": "RenderingBias" + } + }, + "DropShadowEffect": { + "IsCreatable": true, + "Attributes": { + "BlurRadius": "double", + "Direction": "double", + "Opacity": "double", + "ShadowDepth": "double", + "RenderingBias": "RenderingBias", + "Color": "Color" + } + } + }, + "Types": { + "string": {}, + "bool": { + "Values": [ + "True", + "False" + ] + }, + "int": {}, + "double": {}, + "object": { "CanHaveElement": true }, + "Thickness": {}, + "Rect": {}, + "Point": {}, + "CornerRadius": {}, + "Brush": { "CanHaveElement": true }, + "Color": {}, + "ImageSource": {}, + "Transform": { "CanHaveElement": true }, + "FontFamily": {}, + "GridLength": {}, + "Visibility": { + "Values": [ + "Visible", + "Hidden", + "Collapsed" + ] + }, + "HorizontalAlignment": { + "Values": [ + "Left", + "Center", + "Right", + "Stretch" + ] + }, + "VerticalAlignment": { + "Values": [ + "Top", + "Center", + "Bottom", + "Stretch" + ] + }, + "Theme": { + "Values": [ + "Default", + "Dark", + "Light" + ] + }, + "FontWeight": { + "Values": [ + "Thin", + "ExtraLight", + "UltraLight", + "Medium", + "Normal", + "Regular", + "DemiBold", + "SemiBold", + "Bold", + "ExtraBold", + "UltraBold", + "Black", + "Heavy", + "ExtraBlack", + "ExtraHeavy" + ] + }, + "FontStyle": { + "Values": [ + "Normal", + "Italic", + "Oblique" + ] + }, + "LineStackingStrategy": { + "Values": [ + "BlockLineHeight", + "MaxHeight" + ] + }, + "TextAlignment": { + "Values": [ + "Left", + "Right", + "Center", + "Justify" + ] + }, + "TextTrimming": { + "Values": [ + "None", + "CharacterEllipsis", + "WordEllipsis" + ] + }, + "TextWrapping": { + "Values": [ + "WrapWithOverflow", + "NoWrap", + "Wrap" + ] + }, + "TextDecorations": { + "Values": [ + "Baseline", + "OverLine", + "Strikethrough", + "Underline" + ] + }, + "Stretch": { + "Values": [ + "None", + "Fill", + "Uniform", + "UniformToFill" + ] + }, + "StretchDirection": { + "Values": [ + "UpOnly", + "DownOnly", + "Both" + ] + }, + "AlignmentX": { + "Values": [ + "Left", + "Center", + "Right" + ] + }, + "AlignmentY": { + "Values": [ + "Top", + "Center", + "Bottom" + ] + }, + "TileMode": { + "Values": [ + "None", + "FlipX", + "FlipY", + "FlipXY", + "Tile" + ] + }, + "BrushMappingMode": { + "Values": [ + "Absolute", + "RelativeToBoundingBox" + ] + }, + "ColorInterpolationMode": { + "Values": [ + "ScRgbLinearInterpolation", + "SRgbLinearInterpolation" + ] + }, + "GradientSpreadMethod": { + "Values": [ + "Pad", + "Reflect", + "Repeat" + ] + }, + "PenLineCap": { + "Values": [ + "Flat", + "Square", + "Round", + "Triangle" + ] + }, + "PenLineJoin": { + "Values": [ + "Miter", + "Bevel", + "Round" + ] + }, + "KernelType": { + "Values": [ + "Gaussian", + "Box" + ] + }, + "RenderingBias": { + "Values": [ + "Performance", + "Quality" + ] + }, + "Orientation": { + "Values": [ + "Horizontal", + "Vertical" + ] + }, + "WindowCornerPreference": { + "Values": [ + "Default", + "DoNotRound", + "Round", + "RoundSmall" + ] + } + } +} \ No newline at end of file diff --git a/Bloxstrap/Resources/CustomBootstrapperTemplate_Blank.xml b/Bloxstrap/Resources/CustomBootstrapperTemplate_Blank.xml new file mode 100644 index 0000000..3945086 --- /dev/null +++ b/Bloxstrap/Resources/CustomBootstrapperTemplate_Blank.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/Bloxstrap/Resources/CustomBootstrapperTemplate_Simple.xml b/Bloxstrap/Resources/CustomBootstrapperTemplate_Simple.xml new file mode 100644 index 0000000..abe586d --- /dev/null +++ b/Bloxstrap/Resources/CustomBootstrapperTemplate_Simple.xml @@ -0,0 +1,9 @@ + + + + + + + +