From 8e47290e3dbfb81dc6101c0d375a4f5c1bf91124 Mon Sep 17 00:00:00 2001 From: Slenderman Date: Fri, 7 Mar 2025 16:21:49 -0500 Subject: [PATCH] Yep --- Bloxstrap.sln | 32 - Bloxstrap.txt | 1 + Bloxstrap/App.xaml | 42 - Bloxstrap/App.xaml.cs | 359 -- Bloxstrap/AppData/CommonAppData.cs | 74 - Bloxstrap/AppData/IAppData.cs | 21 - Bloxstrap/AppData/RobloxPlayerData.cs | 26 - Bloxstrap/AppData/RobloxStudioData.cs | 36 - Bloxstrap/AssemblyInfo.cs | 10 - Bloxstrap/Bloxstrap.csproj | 114 - Bloxstrap/Bloxstrap.ico | Bin 132843 -> 0 bytes Bloxstrap/Bootstrapper.cs | 1272 ------- Bloxstrap/Enums/BootstrapperIcon.cs | 24 - Bloxstrap/Enums/BootstrapperStyle.cs | 15 - Bloxstrap/Enums/CursorType.cs | 15 - Bloxstrap/Enums/EmojiType.cs | 11 - Bloxstrap/Enums/ErrorCode.cs | 20 - .../Enums/FlagPresets/InGameMenuVersion.cs | 12 - Bloxstrap/Enums/FlagPresets/LightingMode.cs | 10 - Bloxstrap/Enums/FlagPresets/MSAAMode.cs | 14 - Bloxstrap/Enums/FlagPresets/RenderingMode.cs | 10 - Bloxstrap/Enums/FlagPresets/TextureQuality.cs | 12 - Bloxstrap/Enums/GenericTriState.cs | 9 - Bloxstrap/Enums/LaunchMode.cs | 10 - Bloxstrap/Enums/NextAction.cs | 10 - Bloxstrap/Enums/ServerType.cs | 9 - Bloxstrap/Enums/Theme.cs | 10 - Bloxstrap/Enums/VersionComparison.cs | 9 - Bloxstrap/Exceptions/AssertionException.cs | 16 - .../Exceptions/ChecksumFailedException.cs | 15 - .../Exceptions/InvalidChannelException.cs | 10 - .../InvalidHTTPResponseException.cs | 13 - Bloxstrap/Extensions/BootstrapperIconEx.cs | 70 - Bloxstrap/Extensions/BootstrapperStyleEx.cs | 19 - Bloxstrap/Extensions/DateTimeEx.cs | 10 - Bloxstrap/Extensions/EmojiTypeEx.cs | 31 - Bloxstrap/Extensions/IconEx.cs | 35 - Bloxstrap/Extensions/RegistryKeyEx.cs | 35 - Bloxstrap/Extensions/ResourceManagerEx.cs | 24 - Bloxstrap/Extensions/ServerTypeEx.cs | 13 - Bloxstrap/Extensions/ThemeEx.cs | 20 - Bloxstrap/FastFlagManager.cs | 263 -- Bloxstrap/GlobalCache.cs | 7 - Bloxstrap/GlobalUsings.cs | 32 - Bloxstrap/HttpClientLoggingHandler.cs | 22 - Bloxstrap/Installer.cs | 624 --- Bloxstrap/Integrations/ActivityWatcher.cs | 389 -- Bloxstrap/Integrations/DiscordRichPresence.cs | 344 -- Bloxstrap/JsonManager.cs | 93 - Bloxstrap/LaunchHandler.cs | 299 -- Bloxstrap/LaunchSettings.cs | 179 - Bloxstrap/Locale.cs | 133 - Bloxstrap/Logger.cs | 145 - Bloxstrap/Models/APIs/Config/Supporter.cs | 13 - Bloxstrap/Models/APIs/Config/SupporterData.cs | 11 - .../Models/APIs/Config/SupporterGroup.cs | 11 - .../Models/APIs/GitHub/GitHubReleaseAsset.cs | 8 - Bloxstrap/Models/APIs/GitHub/GithubRelease.cs | 20 - Bloxstrap/Models/APIs/IPInfoResponse.cs | 14 - .../Models/APIs/Roblox/ApiArrayResponse.cs | 11 - .../Models/APIs/Roblox/ClientFlagSettings.cs | 8 - Bloxstrap/Models/APIs/Roblox/ClientVersion.cs | 16 - Bloxstrap/Models/APIs/Roblox/GameCreator.cs | 39 - .../Models/APIs/Roblox/GameDetailResponse.cs | 151 - .../Models/APIs/Roblox/GetUserResponse.cs | 56 - .../Models/APIs/Roblox/ThumbnailResponse.cs | 17 - .../Models/APIs/Roblox/UniverseIdResponse.cs | 9 - .../Attributes/BuildMetadataAttribute.cs | 19 - .../Models/Attributes/EnumNameAttribute.cs | 14 - .../Models/Attributes/EnumSortAttribute.cs | 13 - Bloxstrap/Models/BloxstrapRPC/Message.cs | 10 - Bloxstrap/Models/BloxstrapRPC/RichPresence.cs | 23 - .../Models/BloxstrapRPC/RichPresenceImage.cs | 17 - Bloxstrap/Models/BootstrapperIconEntry.cs | 10 - Bloxstrap/Models/CustomIntegration.cs | 10 - Bloxstrap/Models/DeployInfo.cs | 9 - Bloxstrap/Models/Entities/ActivityData.cs | 158 - .../Models/Entities/ModPresetFileData.cs | 40 - Bloxstrap/Models/Entities/UniverseDetails.cs | 53 - Bloxstrap/Models/Entities/UserDetails.cs | 42 - Bloxstrap/Models/FastFlag.cs | 9 - Bloxstrap/Models/FontFace.cs | 17 - Bloxstrap/Models/FontFamily.cs | 11 - Bloxstrap/Models/LaunchFlag.cs | 22 - Bloxstrap/Models/Manifest/FileManifest.cs | 35 - Bloxstrap/Models/Manifest/ManifestFile.cs | 13 - Bloxstrap/Models/Manifest/Package.cs | 26 - Bloxstrap/Models/Manifest/PackageManifest.cs | 50 - Bloxstrap/Models/Persistable/AppState.cs | 11 - Bloxstrap/Models/Persistable/Settings.cs | 32 - Bloxstrap/Models/Persistable/State.cs | 17 - Bloxstrap/Models/Persistable/WindowState.cs | 13 - .../Models/SettingTasks/Base/BaseTask.cs | 23 - .../Models/SettingTasks/Base/BoolBaseTask.cs | 47 - .../Models/SettingTasks/Base/EnumBaseTask.cs | 53 - .../SettingTasks/Base/StringBaseTask.cs | 45 - .../Models/SettingTasks/EmojiModPresetTask.cs | 72 - .../Models/SettingTasks/EnumModPresetTask.cs | 69 - .../Models/SettingTasks/ExtractIconsTask.cs | 43 - .../Models/SettingTasks/FontModPresetTask.cs | 43 - .../Models/SettingTasks/ModPresetTask.cs | 60 - Bloxstrap/Models/SettingTasks/ShortcutTask.cs | 27 - Bloxstrap/Models/WatcherData.cs | 11 - Bloxstrap/NativeMethods.txt | 5 - Bloxstrap/Paths.cs | 44 - .../PublishProfiles/Publish-x64.pubxml | 18 - Bloxstrap/Properties/Resources.Designer.cs | 211 -- Bloxstrap/Properties/Resources.resx | 160 - Bloxstrap/Properties/Settings.Designer.cs | 26 - Bloxstrap/Properties/Settings.settings | 6 - Bloxstrap/Properties/launchSettings.json | 35 - Bloxstrap/Resource.cs | 25 - .../ByfronDialog/ByfronLogoDark.jpg | Bin 6262 -> 0 bytes .../ByfronDialog/ByfronLogoLight.jpg | Bin 5718 -> 0 bytes .../BootstrapperStyles/ByfronDialog/Matt.png | Bin 7500 -> 0 bytes Bloxstrap/Resources/CancelButton.png | Bin 229 -> 0 bytes Bloxstrap/Resources/CancelButtonHover.png | Bin 394 -> 0 bytes Bloxstrap/Resources/DarkCancelButton.png | Bin 229 -> 0 bytes Bloxstrap/Resources/DarkCancelButtonHover.png | Bin 394 -> 0 bytes .../NotoSansThai-VariableFont_wdth,wght.ttf | Bin 217004 -> 0 bytes .../Fonts/Rubik-VariableFont_wght.ttf | Bin 193736 -> 0 bytes Bloxstrap/Resources/Icon2008.ico | Bin 102134 -> 0 bytes Bloxstrap/Resources/Icon2011.ico | Bin 102134 -> 0 bytes Bloxstrap/Resources/Icon2017.ico | Bin 102134 -> 0 bytes Bloxstrap/Resources/Icon2019.ico | Bin 102134 -> 0 bytes Bloxstrap/Resources/Icon2022.ico | Bin 102134 -> 0 bytes Bloxstrap/Resources/IconBloxstrap.ico | Bin 132843 -> 0 bytes Bloxstrap/Resources/IconBloxstrapClassic.ico | Bin 102134 -> 0 bytes Bloxstrap/Resources/IconEarly2015.ico | Bin 102134 -> 0 bytes Bloxstrap/Resources/IconLate2015.ico | Bin 102134 -> 0 bytes Bloxstrap/Resources/MessageBox/Error.png | Bin 1932 -> 0 bytes .../MessageBox/FullQuality/Error.png | Bin 57574 -> 0 bytes .../MessageBox/FullQuality/Information.png | Bin 59501 -> 0 bytes .../MessageBox/FullQuality/Question.png | Bin 60128 -> 0 bytes .../MessageBox/FullQuality/Warning.png | Bin 37678 -> 0 bytes .../Resources/MessageBox/Information.png | Bin 1664 -> 0 bytes Bloxstrap/Resources/MessageBox/Question.png | Bin 1919 -> 0 bytes Bloxstrap/Resources/MessageBox/Warning.png | Bin 6174 -> 0 bytes .../Mods/Cursor/From2006/ArrowCursor.png | Bin 2065 -> 0 bytes .../Mods/Cursor/From2006/ArrowFarCursor.png | Bin 2169 -> 0 bytes .../Mods/Cursor/From2013/ArrowCursor.png | Bin 232 -> 0 bytes .../Mods/Cursor/From2013/ArrowFarCursor.png | Bin 235 -> 0 bytes .../Resources/Mods/OldAvatarBackground.rbxl | Bin 161389 -> 0 bytes Bloxstrap/Resources/Mods/Sounds/Empty.mp3 | Bin 1512 -> 0 bytes Bloxstrap/Resources/Mods/Sounds/OldDeath.ogg | Bin 6271 -> 0 bytes Bloxstrap/Resources/Mods/Sounds/OldGetUp.mp3 | Bin 6149 -> 0 bytes Bloxstrap/Resources/Mods/Sounds/OldJump.mp3 | Bin 5574 -> 0 bytes Bloxstrap/Resources/Mods/Sounds/OldWalk.mp3 | Bin 29826 -> 0 bytes Bloxstrap/Resources/Strings.Designer.cs | 3329 ----------------- Bloxstrap/Resources/Strings.ar.resx | 1268 ------- Bloxstrap/Resources/Strings.bg.resx | 1269 ------- Bloxstrap/Resources/Strings.cs.resx | 1270 ------- Bloxstrap/Resources/Strings.de.resx | 1270 ------- Bloxstrap/Resources/Strings.en-US.resx | 129 - Bloxstrap/Resources/Strings.es-ES.resx | 1269 ------- Bloxstrap/Resources/Strings.fa.resx | 1270 ------- Bloxstrap/Resources/Strings.fi.resx | 1270 ------- Bloxstrap/Resources/Strings.fil.resx | 1267 ------- Bloxstrap/Resources/Strings.fr.resx | 1269 ------- Bloxstrap/Resources/Strings.hr.resx | 1268 ------- Bloxstrap/Resources/Strings.hu.resx | 1268 ------- Bloxstrap/Resources/Strings.id.resx | 1270 ------- Bloxstrap/Resources/Strings.it.resx | 1271 ------- Bloxstrap/Resources/Strings.ja.resx | 1283 ------- Bloxstrap/Resources/Strings.ko.resx | 1270 ------- Bloxstrap/Resources/Strings.lt.resx | 1270 ------- Bloxstrap/Resources/Strings.ms.resx | 1260 ------- Bloxstrap/Resources/Strings.nl.resx | 1269 ------- Bloxstrap/Resources/Strings.pl.resx | 1269 ------- Bloxstrap/Resources/Strings.pt-BR.resx | 1271 ------- Bloxstrap/Resources/Strings.resx | 1273 ------- Bloxstrap/Resources/Strings.ro.resx | 1270 ------- Bloxstrap/Resources/Strings.ru.resx | 1268 ------- Bloxstrap/Resources/Strings.sv-SE.resx | 1270 ------- Bloxstrap/Resources/Strings.th.resx | 1264 ------- Bloxstrap/Resources/Strings.tr.resx | 1267 ------- Bloxstrap/Resources/Strings.uk.resx | 1267 ------- Bloxstrap/Resources/Strings.vi.resx | 1272 ------- Bloxstrap/Resources/Strings.zh-CN.resx | 1293 ------- Bloxstrap/Resources/Strings.zh-TW.resx | 1272 ------- .../RobloxInterfaces/ApplicationSettings.cs | 135 - Bloxstrap/RobloxInterfaces/Deployment.cs | 196 - Bloxstrap/UI/Converters/EnumNameConverter.cs | 51 - Bloxstrap/UI/Converters/RangeConverter.cs | 29 - .../UI/Converters/StringFormatConverter.cs | 33 - Bloxstrap/UI/Elements/About/MainWindow.xaml | 51 - .../UI/Elements/About/MainWindow.xaml.cs | 38 - .../UI/Elements/About/Pages/AboutPage.xaml | 159 - .../UI/Elements/About/Pages/AboutPage.xaml.cs | 48 - .../UI/Elements/About/Pages/LicensesPage.xaml | 72 - .../Elements/About/Pages/LicensesPage.xaml.cs | 13 - .../Elements/About/Pages/SupportersPage.xaml | 132 - .../About/Pages/SupportersPage.xaml.cs | 23 - .../Elements/About/Pages/TranslatorsPage.xaml | 594 --- .../About/Pages/TranslatorsPage.xaml.cs | 13 - Bloxstrap/UI/Elements/Base/WpfUiWindow.cs | 41 - .../Bootstrapper/Base/BaseFunctions.cs | 17 - .../Bootstrapper/Base/WinFormsDialogBase.cs | 159 - .../Elements/Bootstrapper/ByfronDialog.xaml | 50 - .../Bootstrapper/ByfronDialog.xaml.cs | 145 - .../Bootstrapper/ClassicFluentDialog.xaml | 62 - .../Bootstrapper/ClassicFluentDialog.xaml.cs | 123 - .../Elements/Bootstrapper/FluentDialog.xaml | 84 - .../Bootstrapper/FluentDialog.xaml.cs | 141 - .../Bootstrapper/LegacyDialog2008.Designer.cs | 96 - .../Elements/Bootstrapper/LegacyDialog2008.cs | 60 - .../Bootstrapper/LegacyDialog2008.resx | 120 - .../Bootstrapper/LegacyDialog2011.Designer.cs | 111 - .../Elements/Bootstrapper/LegacyDialog2011.cs | 60 - .../Bootstrapper/LegacyDialog2011.resx | 120 - .../Bootstrapper/ProgressDialog.Designer.cs | 129 - .../Elements/Bootstrapper/ProgressDialog.cs | 94 - .../Elements/Bootstrapper/ProgressDialog.resx | 120 - .../Bootstrapper/VistaDialog.Designer.cs | 52 - .../UI/Elements/Bootstrapper/VistaDialog.cs | 140 - .../UI/Elements/Bootstrapper/VistaDialog.resx | 60 - .../Elements/ContextMenu/MenuContainer.xaml | 102 - .../ContextMenu/MenuContainer.xaml.cs | 150 - .../Elements/ContextMenu/ServerHistory.xaml | 105 - .../ContextMenu/ServerHistory.xaml.cs | 21 - .../ContextMenu/ServerInformation.xaml | 60 - .../ContextMenu/ServerInformation.xaml.cs | 31 - Bloxstrap/UI/Elements/Controls/Expander.xaml | 23 - .../UI/Elements/Controls/Expander.xaml.cs | 67 - .../UI/Elements/Controls/MarkdownTextBlock.cs | 134 - .../UI/Elements/Controls/OptionControl.xaml | 70 - .../Elements/Controls/OptionControl.xaml.cs | 66 - .../Elements/Dialogs/AddFastFlagDialog.xaml | 105 - .../Dialogs/AddFastFlagDialog.xaml.cs | 38 - .../Elements/Dialogs/ConnectivityDialog.xaml | 48 - .../Dialogs/ConnectivityDialog.xaml.cs | 86 - .../UI/Elements/Dialogs/ExceptionDialog.xaml | 54 - .../Elements/Dialogs/ExceptionDialog.xaml.cs | 91 - .../UI/Elements/Dialogs/FluentMessageBox.xaml | 48 - .../Elements/Dialogs/FluentMessageBox.xaml.cs | 152 - .../Dialogs/LanguageSelectorDialog.xaml | 42 - .../Dialogs/LanguageSelectorDialog.xaml.cs | 33 - .../UI/Elements/Dialogs/LaunchMenuDialog.xaml | 89 - .../Elements/Dialogs/LaunchMenuDialog.xaml.cs | 41 - .../Elements/Dialogs/UninstallerDialog.xaml | 56 - .../Dialogs/UninstallerDialog.xaml.cs | 44 - .../UI/Elements/Installer/MainWindow.xaml | 81 - .../UI/Elements/Installer/MainWindow.xaml.cs | 143 - .../Installer/Pages/CompletionPage.xaml | 32 - .../Installer/Pages/CompletionPage.xaml.cs | 36 - .../Elements/Installer/Pages/InstallPage.xaml | 78 - .../Installer/Pages/InstallPage.xaml.cs | 39 - .../Elements/Installer/Pages/WelcomePage.xaml | 57 - .../Installer/Pages/WelcomePage.xaml.cs | 33 - .../UI/Elements/Settings/MainWindow.xaml | 112 - .../UI/Elements/Settings/MainWindow.xaml.cs | 113 - .../Settings/Pages/AppearancePage.xaml | 118 - .../Settings/Pages/AppearancePage.xaml.cs | 16 - .../Settings/Pages/BloxstrapPage.xaml | 71 - .../Settings/Pages/BloxstrapPage.xaml.cs | 21 - .../Settings/Pages/BootstrapperPage.xaml | 48 - .../Settings/Pages/BootstrapperPage.xaml.cs | 36 - .../Settings/Pages/FastFlagEditorPage.xaml | 99 - .../Settings/Pages/FastFlagEditorPage.xaml.cs | 407 -- .../Pages/FastFlagEditorWarningPage.xaml | 26 - .../Pages/FastFlagEditorWarningPage.xaml.cs | 31 - .../Settings/Pages/FastFlagsPage.xaml | 159 - .../Settings/Pages/FastFlagsPage.xaml.cs | 62 - .../Settings/Pages/IntegrationsPage.xaml | 127 - .../Settings/Pages/IntegrationsPage.xaml.cs | 25 - .../UI/Elements/Settings/Pages/ModsPage.xaml | 106 - .../Elements/Settings/Pages/ModsPage.xaml.cs | 16 - .../Settings/Pages/ShortcutsPage.xaml | 81 - .../Settings/Pages/ShortcutsPage.xaml.cs | 31 - Bloxstrap/UI/Frontend.cs | 87 - Bloxstrap/UI/IBootstrapperDialog.cs | 22 - Bloxstrap/UI/NotifyIconWrapper.cs | 138 - Bloxstrap/UI/Utility/Rendering.cs | 24 - Bloxstrap/UI/Utility/TaskbarProgress.cs | 90 - Bloxstrap/UI/Utility/WindowScaling.cs | 30 - .../UI/ViewModels/About/AboutViewModel.cs | 17 - .../ViewModels/About/SupportersViewModel.cs | 68 - .../BootstrapperDialogViewModel.cs | 46 - .../Bootstrapper/ByfronDialogViewModel.cs | 26 - .../ClassicFluentDialogViewModel.cs | 15 - .../Bootstrapper/FluentDialogViewModel.cs | 35 - .../ContextMenu/ServerHistoryViewModel.cs | 94 - .../ContextMenu/ServerInformationViewModel.cs | 44 - .../Dialogs/LanguageSelectorViewModel.cs | 32 - .../ViewModels/Dialogs/LaunchMenuViewModel.cs | 33 - .../Dialogs/UninstallerViewModel.cs | 24 - Bloxstrap/UI/ViewModels/GlobalViewModel.cs | 18 - .../Installer/CompletionViewModel.cs | 23 - .../ViewModels/Installer/InstallViewModel.cs | 100 - .../Installer/MainWindowViewModel.cs | 52 - .../ViewModels/Installer/WelcomeViewModel.cs | 38 - .../NotifyPropertyChangedViewModel.cs | 10 - .../Settings/AppearanceViewModel.cs | 120 - .../ViewModels/Settings/BehaviourViewModel.cs | 42 - .../ViewModels/Settings/BloxstrapViewModel.cs | 91 - .../FastFlagEditorWarningViewModel.cs | 90 - .../ViewModels/Settings/FastFlagsViewModel.cs | 174 - .../Settings/IntegrationsViewModel.cs | 138 - .../Settings/MainWindowViewModel.cs | 65 - .../UI/ViewModels/Settings/ModsViewModel.cs | 118 - .../ViewModels/Settings/ShortcutsViewModel.cs | 19 - Bloxstrap/Utilities.cs | 111 - Bloxstrap/Utility/AsyncMutex.cs | 97 - Bloxstrap/Utility/Filesystem.cs | 35 - Bloxstrap/Utility/Http.cs | 23 - Bloxstrap/Utility/InterProcessLock.cs | 42 - Bloxstrap/Utility/MD5Hash.cs | 33 - Bloxstrap/Utility/Shortcut.cs | 38 - Bloxstrap/Utility/WindowsRegistry.cs | 123 - Bloxstrap/Watcher.cs | 131 - Bloxstrap/app.manifest | 77 - Images/Bloxstrap-full-dark.png | Bin 229176 -> 0 bytes Images/Bloxstrap-full-light.png | Bin 223361 -> 0 bytes Images/Bloxstrap.png | Bin 157504 -> 0 bytes LICENSE | 21 - README.md | 85 - Scripts/Translations/find-unused.py | 39 - Scripts/Translations/prep.py | 11 - wpfui | 1 - 319 files changed, 1 insertion(+), 58994 deletions(-) delete mode 100644 Bloxstrap.sln create mode 100644 Bloxstrap.txt delete mode 100644 Bloxstrap/App.xaml delete mode 100644 Bloxstrap/App.xaml.cs delete mode 100644 Bloxstrap/AppData/CommonAppData.cs delete mode 100644 Bloxstrap/AppData/IAppData.cs delete mode 100644 Bloxstrap/AppData/RobloxPlayerData.cs delete mode 100644 Bloxstrap/AppData/RobloxStudioData.cs delete mode 100644 Bloxstrap/AssemblyInfo.cs delete mode 100644 Bloxstrap/Bloxstrap.csproj delete mode 100644 Bloxstrap/Bloxstrap.ico delete mode 100644 Bloxstrap/Bootstrapper.cs delete mode 100644 Bloxstrap/Enums/BootstrapperIcon.cs delete mode 100644 Bloxstrap/Enums/BootstrapperStyle.cs delete mode 100644 Bloxstrap/Enums/CursorType.cs delete mode 100644 Bloxstrap/Enums/EmojiType.cs delete mode 100644 Bloxstrap/Enums/ErrorCode.cs delete mode 100644 Bloxstrap/Enums/FlagPresets/InGameMenuVersion.cs delete mode 100644 Bloxstrap/Enums/FlagPresets/LightingMode.cs delete mode 100644 Bloxstrap/Enums/FlagPresets/MSAAMode.cs delete mode 100644 Bloxstrap/Enums/FlagPresets/RenderingMode.cs delete mode 100644 Bloxstrap/Enums/FlagPresets/TextureQuality.cs delete mode 100644 Bloxstrap/Enums/GenericTriState.cs delete mode 100644 Bloxstrap/Enums/LaunchMode.cs delete mode 100644 Bloxstrap/Enums/NextAction.cs delete mode 100644 Bloxstrap/Enums/ServerType.cs delete mode 100644 Bloxstrap/Enums/Theme.cs delete mode 100644 Bloxstrap/Enums/VersionComparison.cs delete mode 100644 Bloxstrap/Exceptions/AssertionException.cs delete mode 100644 Bloxstrap/Exceptions/ChecksumFailedException.cs delete mode 100644 Bloxstrap/Exceptions/InvalidChannelException.cs delete mode 100644 Bloxstrap/Exceptions/InvalidHTTPResponseException.cs delete mode 100644 Bloxstrap/Extensions/BootstrapperIconEx.cs delete mode 100644 Bloxstrap/Extensions/BootstrapperStyleEx.cs delete mode 100644 Bloxstrap/Extensions/DateTimeEx.cs delete mode 100644 Bloxstrap/Extensions/EmojiTypeEx.cs delete mode 100644 Bloxstrap/Extensions/IconEx.cs delete mode 100644 Bloxstrap/Extensions/RegistryKeyEx.cs delete mode 100644 Bloxstrap/Extensions/ResourceManagerEx.cs delete mode 100644 Bloxstrap/Extensions/ServerTypeEx.cs delete mode 100644 Bloxstrap/Extensions/ThemeEx.cs delete mode 100644 Bloxstrap/FastFlagManager.cs delete mode 100644 Bloxstrap/GlobalCache.cs delete mode 100644 Bloxstrap/GlobalUsings.cs delete mode 100644 Bloxstrap/HttpClientLoggingHandler.cs delete mode 100644 Bloxstrap/Installer.cs delete mode 100644 Bloxstrap/Integrations/ActivityWatcher.cs delete mode 100644 Bloxstrap/Integrations/DiscordRichPresence.cs delete mode 100644 Bloxstrap/JsonManager.cs delete mode 100644 Bloxstrap/LaunchHandler.cs delete mode 100644 Bloxstrap/LaunchSettings.cs delete mode 100644 Bloxstrap/Locale.cs delete mode 100644 Bloxstrap/Logger.cs delete mode 100644 Bloxstrap/Models/APIs/Config/Supporter.cs delete mode 100644 Bloxstrap/Models/APIs/Config/SupporterData.cs delete mode 100644 Bloxstrap/Models/APIs/Config/SupporterGroup.cs delete mode 100644 Bloxstrap/Models/APIs/GitHub/GitHubReleaseAsset.cs delete mode 100644 Bloxstrap/Models/APIs/GitHub/GithubRelease.cs delete mode 100644 Bloxstrap/Models/APIs/IPInfoResponse.cs delete mode 100644 Bloxstrap/Models/APIs/Roblox/ApiArrayResponse.cs delete mode 100644 Bloxstrap/Models/APIs/Roblox/ClientFlagSettings.cs delete mode 100644 Bloxstrap/Models/APIs/Roblox/ClientVersion.cs delete mode 100644 Bloxstrap/Models/APIs/Roblox/GameCreator.cs delete mode 100644 Bloxstrap/Models/APIs/Roblox/GameDetailResponse.cs delete mode 100644 Bloxstrap/Models/APIs/Roblox/GetUserResponse.cs delete mode 100644 Bloxstrap/Models/APIs/Roblox/ThumbnailResponse.cs delete mode 100644 Bloxstrap/Models/APIs/Roblox/UniverseIdResponse.cs delete mode 100644 Bloxstrap/Models/Attributes/BuildMetadataAttribute.cs delete mode 100644 Bloxstrap/Models/Attributes/EnumNameAttribute.cs delete mode 100644 Bloxstrap/Models/Attributes/EnumSortAttribute.cs delete mode 100644 Bloxstrap/Models/BloxstrapRPC/Message.cs delete mode 100644 Bloxstrap/Models/BloxstrapRPC/RichPresence.cs delete mode 100644 Bloxstrap/Models/BloxstrapRPC/RichPresenceImage.cs delete mode 100644 Bloxstrap/Models/BootstrapperIconEntry.cs delete mode 100644 Bloxstrap/Models/CustomIntegration.cs delete mode 100644 Bloxstrap/Models/DeployInfo.cs delete mode 100644 Bloxstrap/Models/Entities/ActivityData.cs delete mode 100644 Bloxstrap/Models/Entities/ModPresetFileData.cs delete mode 100644 Bloxstrap/Models/Entities/UniverseDetails.cs delete mode 100644 Bloxstrap/Models/Entities/UserDetails.cs delete mode 100644 Bloxstrap/Models/FastFlag.cs delete mode 100644 Bloxstrap/Models/FontFace.cs delete mode 100644 Bloxstrap/Models/FontFamily.cs delete mode 100644 Bloxstrap/Models/LaunchFlag.cs delete mode 100644 Bloxstrap/Models/Manifest/FileManifest.cs delete mode 100644 Bloxstrap/Models/Manifest/ManifestFile.cs delete mode 100644 Bloxstrap/Models/Manifest/Package.cs delete mode 100644 Bloxstrap/Models/Manifest/PackageManifest.cs delete mode 100644 Bloxstrap/Models/Persistable/AppState.cs delete mode 100644 Bloxstrap/Models/Persistable/Settings.cs delete mode 100644 Bloxstrap/Models/Persistable/State.cs delete mode 100644 Bloxstrap/Models/Persistable/WindowState.cs delete mode 100644 Bloxstrap/Models/SettingTasks/Base/BaseTask.cs delete mode 100644 Bloxstrap/Models/SettingTasks/Base/BoolBaseTask.cs delete mode 100644 Bloxstrap/Models/SettingTasks/Base/EnumBaseTask.cs delete mode 100644 Bloxstrap/Models/SettingTasks/Base/StringBaseTask.cs delete mode 100644 Bloxstrap/Models/SettingTasks/EmojiModPresetTask.cs delete mode 100644 Bloxstrap/Models/SettingTasks/EnumModPresetTask.cs delete mode 100644 Bloxstrap/Models/SettingTasks/ExtractIconsTask.cs delete mode 100644 Bloxstrap/Models/SettingTasks/FontModPresetTask.cs delete mode 100644 Bloxstrap/Models/SettingTasks/ModPresetTask.cs delete mode 100644 Bloxstrap/Models/SettingTasks/ShortcutTask.cs delete mode 100644 Bloxstrap/Models/WatcherData.cs delete mode 100644 Bloxstrap/NativeMethods.txt delete mode 100644 Bloxstrap/Paths.cs delete mode 100644 Bloxstrap/Properties/PublishProfiles/Publish-x64.pubxml delete mode 100644 Bloxstrap/Properties/Resources.Designer.cs delete mode 100644 Bloxstrap/Properties/Resources.resx delete mode 100644 Bloxstrap/Properties/Settings.Designer.cs delete mode 100644 Bloxstrap/Properties/Settings.settings delete mode 100644 Bloxstrap/Properties/launchSettings.json delete mode 100644 Bloxstrap/Resource.cs delete mode 100644 Bloxstrap/Resources/BootstrapperStyles/ByfronDialog/ByfronLogoDark.jpg delete mode 100644 Bloxstrap/Resources/BootstrapperStyles/ByfronDialog/ByfronLogoLight.jpg delete mode 100644 Bloxstrap/Resources/BootstrapperStyles/ByfronDialog/Matt.png delete mode 100644 Bloxstrap/Resources/CancelButton.png delete mode 100644 Bloxstrap/Resources/CancelButtonHover.png delete mode 100644 Bloxstrap/Resources/DarkCancelButton.png delete mode 100644 Bloxstrap/Resources/DarkCancelButtonHover.png delete mode 100644 Bloxstrap/Resources/Fonts/NotoSansThai-VariableFont_wdth,wght.ttf delete mode 100644 Bloxstrap/Resources/Fonts/Rubik-VariableFont_wght.ttf delete mode 100644 Bloxstrap/Resources/Icon2008.ico delete mode 100644 Bloxstrap/Resources/Icon2011.ico delete mode 100644 Bloxstrap/Resources/Icon2017.ico delete mode 100644 Bloxstrap/Resources/Icon2019.ico delete mode 100644 Bloxstrap/Resources/Icon2022.ico delete mode 100644 Bloxstrap/Resources/IconBloxstrap.ico delete mode 100644 Bloxstrap/Resources/IconBloxstrapClassic.ico delete mode 100644 Bloxstrap/Resources/IconEarly2015.ico delete mode 100644 Bloxstrap/Resources/IconLate2015.ico delete mode 100644 Bloxstrap/Resources/MessageBox/Error.png delete mode 100644 Bloxstrap/Resources/MessageBox/FullQuality/Error.png delete mode 100644 Bloxstrap/Resources/MessageBox/FullQuality/Information.png delete mode 100644 Bloxstrap/Resources/MessageBox/FullQuality/Question.png delete mode 100644 Bloxstrap/Resources/MessageBox/FullQuality/Warning.png delete mode 100644 Bloxstrap/Resources/MessageBox/Information.png delete mode 100644 Bloxstrap/Resources/MessageBox/Question.png delete mode 100644 Bloxstrap/Resources/MessageBox/Warning.png delete mode 100644 Bloxstrap/Resources/Mods/Cursor/From2006/ArrowCursor.png delete mode 100644 Bloxstrap/Resources/Mods/Cursor/From2006/ArrowFarCursor.png delete mode 100644 Bloxstrap/Resources/Mods/Cursor/From2013/ArrowCursor.png delete mode 100644 Bloxstrap/Resources/Mods/Cursor/From2013/ArrowFarCursor.png delete mode 100644 Bloxstrap/Resources/Mods/OldAvatarBackground.rbxl delete mode 100644 Bloxstrap/Resources/Mods/Sounds/Empty.mp3 delete mode 100644 Bloxstrap/Resources/Mods/Sounds/OldDeath.ogg delete mode 100644 Bloxstrap/Resources/Mods/Sounds/OldGetUp.mp3 delete mode 100644 Bloxstrap/Resources/Mods/Sounds/OldJump.mp3 delete mode 100644 Bloxstrap/Resources/Mods/Sounds/OldWalk.mp3 delete mode 100644 Bloxstrap/Resources/Strings.Designer.cs delete mode 100644 Bloxstrap/Resources/Strings.ar.resx delete mode 100644 Bloxstrap/Resources/Strings.bg.resx delete mode 100644 Bloxstrap/Resources/Strings.cs.resx delete mode 100644 Bloxstrap/Resources/Strings.de.resx delete mode 100644 Bloxstrap/Resources/Strings.en-US.resx delete mode 100644 Bloxstrap/Resources/Strings.es-ES.resx delete mode 100644 Bloxstrap/Resources/Strings.fa.resx delete mode 100644 Bloxstrap/Resources/Strings.fi.resx delete mode 100644 Bloxstrap/Resources/Strings.fil.resx delete mode 100644 Bloxstrap/Resources/Strings.fr.resx delete mode 100644 Bloxstrap/Resources/Strings.hr.resx delete mode 100644 Bloxstrap/Resources/Strings.hu.resx delete mode 100644 Bloxstrap/Resources/Strings.id.resx delete mode 100644 Bloxstrap/Resources/Strings.it.resx delete mode 100644 Bloxstrap/Resources/Strings.ja.resx delete mode 100644 Bloxstrap/Resources/Strings.ko.resx delete mode 100644 Bloxstrap/Resources/Strings.lt.resx delete mode 100644 Bloxstrap/Resources/Strings.ms.resx delete mode 100644 Bloxstrap/Resources/Strings.nl.resx delete mode 100644 Bloxstrap/Resources/Strings.pl.resx delete mode 100644 Bloxstrap/Resources/Strings.pt-BR.resx delete mode 100644 Bloxstrap/Resources/Strings.resx delete mode 100644 Bloxstrap/Resources/Strings.ro.resx delete mode 100644 Bloxstrap/Resources/Strings.ru.resx delete mode 100644 Bloxstrap/Resources/Strings.sv-SE.resx delete mode 100644 Bloxstrap/Resources/Strings.th.resx delete mode 100644 Bloxstrap/Resources/Strings.tr.resx delete mode 100644 Bloxstrap/Resources/Strings.uk.resx delete mode 100644 Bloxstrap/Resources/Strings.vi.resx delete mode 100644 Bloxstrap/Resources/Strings.zh-CN.resx delete mode 100644 Bloxstrap/Resources/Strings.zh-TW.resx delete mode 100644 Bloxstrap/RobloxInterfaces/ApplicationSettings.cs delete mode 100644 Bloxstrap/RobloxInterfaces/Deployment.cs delete mode 100644 Bloxstrap/UI/Converters/EnumNameConverter.cs delete mode 100644 Bloxstrap/UI/Converters/RangeConverter.cs delete mode 100644 Bloxstrap/UI/Converters/StringFormatConverter.cs delete mode 100644 Bloxstrap/UI/Elements/About/MainWindow.xaml delete mode 100644 Bloxstrap/UI/Elements/About/MainWindow.xaml.cs delete mode 100644 Bloxstrap/UI/Elements/About/Pages/AboutPage.xaml delete mode 100644 Bloxstrap/UI/Elements/About/Pages/AboutPage.xaml.cs delete mode 100644 Bloxstrap/UI/Elements/About/Pages/LicensesPage.xaml delete mode 100644 Bloxstrap/UI/Elements/About/Pages/LicensesPage.xaml.cs delete mode 100644 Bloxstrap/UI/Elements/About/Pages/SupportersPage.xaml delete mode 100644 Bloxstrap/UI/Elements/About/Pages/SupportersPage.xaml.cs delete mode 100644 Bloxstrap/UI/Elements/About/Pages/TranslatorsPage.xaml delete mode 100644 Bloxstrap/UI/Elements/About/Pages/TranslatorsPage.xaml.cs delete mode 100644 Bloxstrap/UI/Elements/Base/WpfUiWindow.cs delete mode 100644 Bloxstrap/UI/Elements/Bootstrapper/Base/BaseFunctions.cs delete mode 100644 Bloxstrap/UI/Elements/Bootstrapper/Base/WinFormsDialogBase.cs delete mode 100644 Bloxstrap/UI/Elements/Bootstrapper/ByfronDialog.xaml delete mode 100644 Bloxstrap/UI/Elements/Bootstrapper/ByfronDialog.xaml.cs delete mode 100644 Bloxstrap/UI/Elements/Bootstrapper/ClassicFluentDialog.xaml delete mode 100644 Bloxstrap/UI/Elements/Bootstrapper/ClassicFluentDialog.xaml.cs delete mode 100644 Bloxstrap/UI/Elements/Bootstrapper/FluentDialog.xaml delete mode 100644 Bloxstrap/UI/Elements/Bootstrapper/FluentDialog.xaml.cs delete mode 100644 Bloxstrap/UI/Elements/Bootstrapper/LegacyDialog2008.Designer.cs delete mode 100644 Bloxstrap/UI/Elements/Bootstrapper/LegacyDialog2008.cs delete mode 100644 Bloxstrap/UI/Elements/Bootstrapper/LegacyDialog2008.resx delete mode 100644 Bloxstrap/UI/Elements/Bootstrapper/LegacyDialog2011.Designer.cs delete mode 100644 Bloxstrap/UI/Elements/Bootstrapper/LegacyDialog2011.cs delete mode 100644 Bloxstrap/UI/Elements/Bootstrapper/LegacyDialog2011.resx delete mode 100644 Bloxstrap/UI/Elements/Bootstrapper/ProgressDialog.Designer.cs delete mode 100644 Bloxstrap/UI/Elements/Bootstrapper/ProgressDialog.cs delete mode 100644 Bloxstrap/UI/Elements/Bootstrapper/ProgressDialog.resx delete mode 100644 Bloxstrap/UI/Elements/Bootstrapper/VistaDialog.Designer.cs delete mode 100644 Bloxstrap/UI/Elements/Bootstrapper/VistaDialog.cs delete mode 100644 Bloxstrap/UI/Elements/Bootstrapper/VistaDialog.resx delete mode 100644 Bloxstrap/UI/Elements/ContextMenu/MenuContainer.xaml delete mode 100644 Bloxstrap/UI/Elements/ContextMenu/MenuContainer.xaml.cs delete mode 100644 Bloxstrap/UI/Elements/ContextMenu/ServerHistory.xaml delete mode 100644 Bloxstrap/UI/Elements/ContextMenu/ServerHistory.xaml.cs delete mode 100644 Bloxstrap/UI/Elements/ContextMenu/ServerInformation.xaml delete mode 100644 Bloxstrap/UI/Elements/ContextMenu/ServerInformation.xaml.cs delete mode 100644 Bloxstrap/UI/Elements/Controls/Expander.xaml delete mode 100644 Bloxstrap/UI/Elements/Controls/Expander.xaml.cs delete mode 100644 Bloxstrap/UI/Elements/Controls/MarkdownTextBlock.cs delete mode 100644 Bloxstrap/UI/Elements/Controls/OptionControl.xaml delete mode 100644 Bloxstrap/UI/Elements/Controls/OptionControl.xaml.cs delete mode 100644 Bloxstrap/UI/Elements/Dialogs/AddFastFlagDialog.xaml delete mode 100644 Bloxstrap/UI/Elements/Dialogs/AddFastFlagDialog.xaml.cs delete mode 100644 Bloxstrap/UI/Elements/Dialogs/ConnectivityDialog.xaml delete mode 100644 Bloxstrap/UI/Elements/Dialogs/ConnectivityDialog.xaml.cs delete mode 100644 Bloxstrap/UI/Elements/Dialogs/ExceptionDialog.xaml delete mode 100644 Bloxstrap/UI/Elements/Dialogs/ExceptionDialog.xaml.cs delete mode 100644 Bloxstrap/UI/Elements/Dialogs/FluentMessageBox.xaml delete mode 100644 Bloxstrap/UI/Elements/Dialogs/FluentMessageBox.xaml.cs delete mode 100644 Bloxstrap/UI/Elements/Dialogs/LanguageSelectorDialog.xaml delete mode 100644 Bloxstrap/UI/Elements/Dialogs/LanguageSelectorDialog.xaml.cs delete mode 100644 Bloxstrap/UI/Elements/Dialogs/LaunchMenuDialog.xaml delete mode 100644 Bloxstrap/UI/Elements/Dialogs/LaunchMenuDialog.xaml.cs delete mode 100644 Bloxstrap/UI/Elements/Dialogs/UninstallerDialog.xaml delete mode 100644 Bloxstrap/UI/Elements/Dialogs/UninstallerDialog.xaml.cs delete mode 100644 Bloxstrap/UI/Elements/Installer/MainWindow.xaml delete mode 100644 Bloxstrap/UI/Elements/Installer/MainWindow.xaml.cs delete mode 100644 Bloxstrap/UI/Elements/Installer/Pages/CompletionPage.xaml delete mode 100644 Bloxstrap/UI/Elements/Installer/Pages/CompletionPage.xaml.cs delete mode 100644 Bloxstrap/UI/Elements/Installer/Pages/InstallPage.xaml delete mode 100644 Bloxstrap/UI/Elements/Installer/Pages/InstallPage.xaml.cs delete mode 100644 Bloxstrap/UI/Elements/Installer/Pages/WelcomePage.xaml delete mode 100644 Bloxstrap/UI/Elements/Installer/Pages/WelcomePage.xaml.cs delete mode 100644 Bloxstrap/UI/Elements/Settings/MainWindow.xaml delete mode 100644 Bloxstrap/UI/Elements/Settings/MainWindow.xaml.cs delete mode 100644 Bloxstrap/UI/Elements/Settings/Pages/AppearancePage.xaml delete mode 100644 Bloxstrap/UI/Elements/Settings/Pages/AppearancePage.xaml.cs delete mode 100644 Bloxstrap/UI/Elements/Settings/Pages/BloxstrapPage.xaml delete mode 100644 Bloxstrap/UI/Elements/Settings/Pages/BloxstrapPage.xaml.cs delete mode 100644 Bloxstrap/UI/Elements/Settings/Pages/BootstrapperPage.xaml delete mode 100644 Bloxstrap/UI/Elements/Settings/Pages/BootstrapperPage.xaml.cs delete mode 100644 Bloxstrap/UI/Elements/Settings/Pages/FastFlagEditorPage.xaml delete mode 100644 Bloxstrap/UI/Elements/Settings/Pages/FastFlagEditorPage.xaml.cs delete mode 100644 Bloxstrap/UI/Elements/Settings/Pages/FastFlagEditorWarningPage.xaml delete mode 100644 Bloxstrap/UI/Elements/Settings/Pages/FastFlagEditorWarningPage.xaml.cs delete mode 100644 Bloxstrap/UI/Elements/Settings/Pages/FastFlagsPage.xaml delete mode 100644 Bloxstrap/UI/Elements/Settings/Pages/FastFlagsPage.xaml.cs delete mode 100644 Bloxstrap/UI/Elements/Settings/Pages/IntegrationsPage.xaml delete mode 100644 Bloxstrap/UI/Elements/Settings/Pages/IntegrationsPage.xaml.cs delete mode 100644 Bloxstrap/UI/Elements/Settings/Pages/ModsPage.xaml delete mode 100644 Bloxstrap/UI/Elements/Settings/Pages/ModsPage.xaml.cs delete mode 100644 Bloxstrap/UI/Elements/Settings/Pages/ShortcutsPage.xaml delete mode 100644 Bloxstrap/UI/Elements/Settings/Pages/ShortcutsPage.xaml.cs delete mode 100644 Bloxstrap/UI/Frontend.cs delete mode 100644 Bloxstrap/UI/IBootstrapperDialog.cs delete mode 100644 Bloxstrap/UI/NotifyIconWrapper.cs delete mode 100644 Bloxstrap/UI/Utility/Rendering.cs delete mode 100644 Bloxstrap/UI/Utility/TaskbarProgress.cs delete mode 100644 Bloxstrap/UI/Utility/WindowScaling.cs delete mode 100644 Bloxstrap/UI/ViewModels/About/AboutViewModel.cs delete mode 100644 Bloxstrap/UI/ViewModels/About/SupportersViewModel.cs delete mode 100644 Bloxstrap/UI/ViewModels/Bootstrapper/BootstrapperDialogViewModel.cs delete mode 100644 Bloxstrap/UI/ViewModels/Bootstrapper/ByfronDialogViewModel.cs delete mode 100644 Bloxstrap/UI/ViewModels/Bootstrapper/ClassicFluentDialogViewModel.cs delete mode 100644 Bloxstrap/UI/ViewModels/Bootstrapper/FluentDialogViewModel.cs delete mode 100644 Bloxstrap/UI/ViewModels/ContextMenu/ServerHistoryViewModel.cs delete mode 100644 Bloxstrap/UI/ViewModels/ContextMenu/ServerInformationViewModel.cs delete mode 100644 Bloxstrap/UI/ViewModels/Dialogs/LanguageSelectorViewModel.cs delete mode 100644 Bloxstrap/UI/ViewModels/Dialogs/LaunchMenuViewModel.cs delete mode 100644 Bloxstrap/UI/ViewModels/Dialogs/UninstallerViewModel.cs delete mode 100644 Bloxstrap/UI/ViewModels/GlobalViewModel.cs delete mode 100644 Bloxstrap/UI/ViewModels/Installer/CompletionViewModel.cs delete mode 100644 Bloxstrap/UI/ViewModels/Installer/InstallViewModel.cs delete mode 100644 Bloxstrap/UI/ViewModels/Installer/MainWindowViewModel.cs delete mode 100644 Bloxstrap/UI/ViewModels/Installer/WelcomeViewModel.cs delete mode 100644 Bloxstrap/UI/ViewModels/NotifyPropertyChangedViewModel.cs delete mode 100644 Bloxstrap/UI/ViewModels/Settings/AppearanceViewModel.cs delete mode 100644 Bloxstrap/UI/ViewModels/Settings/BehaviourViewModel.cs delete mode 100644 Bloxstrap/UI/ViewModels/Settings/BloxstrapViewModel.cs delete mode 100644 Bloxstrap/UI/ViewModels/Settings/FastFlagEditorWarningViewModel.cs delete mode 100644 Bloxstrap/UI/ViewModels/Settings/FastFlagsViewModel.cs delete mode 100644 Bloxstrap/UI/ViewModels/Settings/IntegrationsViewModel.cs delete mode 100644 Bloxstrap/UI/ViewModels/Settings/MainWindowViewModel.cs delete mode 100644 Bloxstrap/UI/ViewModels/Settings/ModsViewModel.cs delete mode 100644 Bloxstrap/UI/ViewModels/Settings/ShortcutsViewModel.cs delete mode 100644 Bloxstrap/Utilities.cs delete mode 100644 Bloxstrap/Utility/AsyncMutex.cs delete mode 100644 Bloxstrap/Utility/Filesystem.cs delete mode 100644 Bloxstrap/Utility/Http.cs delete mode 100644 Bloxstrap/Utility/InterProcessLock.cs delete mode 100644 Bloxstrap/Utility/MD5Hash.cs delete mode 100644 Bloxstrap/Utility/Shortcut.cs delete mode 100644 Bloxstrap/Utility/WindowsRegistry.cs delete mode 100644 Bloxstrap/Watcher.cs delete mode 100644 Bloxstrap/app.manifest delete mode 100644 Images/Bloxstrap-full-dark.png delete mode 100644 Images/Bloxstrap-full-light.png delete mode 100644 Images/Bloxstrap.png delete mode 100644 LICENSE delete mode 100644 README.md delete mode 100644 Scripts/Translations/find-unused.py delete mode 100644 Scripts/Translations/prep.py delete mode 160000 wpfui diff --git a/Bloxstrap.sln b/Bloxstrap.sln deleted file mode 100644 index f425bc2..0000000 --- a/Bloxstrap.sln +++ /dev/null @@ -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 diff --git a/Bloxstrap.txt b/Bloxstrap.txt new file mode 100644 index 0000000..5e7294e --- /dev/null +++ b/Bloxstrap.txt @@ -0,0 +1 @@ +IT WORKS \ No newline at end of file diff --git a/Bloxstrap/App.xaml b/Bloxstrap/App.xaml deleted file mode 100644 index ae4bfac..0000000 --- a/Bloxstrap/App.xaml +++ /dev/null @@ -1,42 +0,0 @@ - - - - - - - - - pack://application:,,,/Resources/Fonts/#Rubik Light - - - - - - - - - diff --git a/Bloxstrap/App.xaml.cs b/Bloxstrap/App.xaml.cs deleted file mode 100644 index 691d868..0000000 --- a/Bloxstrap/App.xaml.cs +++ /dev/null @@ -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 -{ - /// - /// Interaction logic for App.xaml - /// - 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()!; - - 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 PendingSettingTasks = new(); - - public static readonly JsonManager Settings = new(); - - public static readonly JsonManager 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 GetLatestRelease() - { - const string LOG_IDENT = "App::GetLatestRelease"; - - try - { - var releaseInfo = await Http.GetJson($"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"); - } - } -} diff --git a/Bloxstrap/AppData/CommonAppData.cs b/Bloxstrap/AppData/CommonAppData.cs deleted file mode 100644 index 1e1b425..0000000 --- a/Bloxstrap/AppData/CommonAppData.cs +++ /dev/null @@ -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 _commonMap { get; } = new Dictionary() - { - { "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 PackageDirectoryMap { get; set; } - - - public CommonAppData() - { - if (PackageDirectoryMap is null) - { - PackageDirectoryMap = _commonMap; - return; - } - - var merged = new Dictionary(); - - foreach (var entry in _commonMap) - merged[entry.Key] = entry.Value; - - foreach (var entry in PackageDirectoryMap) - merged[entry.Key] = entry.Value; - - PackageDirectoryMap = merged; - } - } -} diff --git a/Bloxstrap/AppData/IAppData.cs b/Bloxstrap/AppData/IAppData.cs deleted file mode 100644 index b19aa95..0000000 --- a/Bloxstrap/AppData/IAppData.cs +++ /dev/null @@ -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 PackageDirectoryMap { get; set; } - } -} diff --git a/Bloxstrap/AppData/RobloxPlayerData.cs b/Bloxstrap/AppData/RobloxPlayerData.cs deleted file mode 100644 index 3c4f728..0000000 --- a/Bloxstrap/AppData/RobloxPlayerData.cs +++ /dev/null @@ -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 PackageDirectoryMap { get; set; } = new Dictionary() - { - { "RobloxApp.zip", @"" } - }; - } -} diff --git a/Bloxstrap/AppData/RobloxStudioData.cs b/Bloxstrap/AppData/RobloxStudioData.cs deleted file mode 100644 index 2ada1c2..0000000 --- a/Bloxstrap/AppData/RobloxStudioData.cs +++ /dev/null @@ -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 PackageDirectoryMap { get; set; } = new Dictionary() - { - { "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\" } - }; - } -} diff --git a/Bloxstrap/AssemblyInfo.cs b/Bloxstrap/AssemblyInfo.cs deleted file mode 100644 index 8b5504e..0000000 --- a/Bloxstrap/AssemblyInfo.cs +++ /dev/null @@ -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) -)] diff --git a/Bloxstrap/Bloxstrap.csproj b/Bloxstrap/Bloxstrap.csproj deleted file mode 100644 index 8e88aa5..0000000 --- a/Bloxstrap/Bloxstrap.csproj +++ /dev/null @@ -1,114 +0,0 @@ - - - - WinExe - net6.0-windows - enable - true - True - Bloxstrap.ico - 2.9.0 - 2.9.0 - app.manifest - true - false - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - all - - - - - - - - - - - - - <_Parameter1>$([System.DateTime]::UtcNow.ToString("s"))Z - <_Parameter2>$(COMPUTERNAME)\$(USERNAME) - <_Parameter3>$(CommitHash) - <_Parameter4>$(CommitRef) - - - - - - - - - - - - - - - @(ProjectionMetadataWinmd->'%(FullPath)','|') - @(ProjectionDocs->'%(FullPath)','|') - - - - - - - - - - - - True - True - Strings.resx - - - - - - PublicResXFileCodeGenerator - Strings.Designer.cs - - - - diff --git a/Bloxstrap/Bloxstrap.ico b/Bloxstrap/Bloxstrap.ico deleted file mode 100644 index a9a04b95662dacddd748311ef4826b4a8ae33b31..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 132843 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 S;VJEV0fzdfdSC9i#QzVNHNwvT diff --git a/Bloxstrap/Bootstrapper.cs b/Bloxstrap/Bootstrapper.cs deleted file mode 100644 index da21e53..0000000 --- a/Bloxstrap/Bootstrapper.cs +++ /dev/null @@ -1,1272 +0,0 @@ -// To debug the automatic updater: -// - Uncomment the definition below -// - Publish the executable -// - Launch the executable (click no when it asks you to upgrade) -// - Launch Roblox (for testing web launches, run it from the command prompt) -// - To re-test the same executable, delete it from the installation folder - -// #define DEBUG_UPDATER - -#if DEBUG_UPDATER -#warning "Automatic updater debugging is enabled" -#endif - -using System.ComponentModel; -using System.Data; -using System.Windows; -using System.Windows.Forms; -using System.Windows.Shell; - -using Microsoft.Win32; - -using Bloxstrap.AppData; -using Bloxstrap.RobloxInterfaces; -using Bloxstrap.UI.Elements.Bootstrapper.Base; - -using ICSharpCode.SharpZipLib.Zip; - -namespace Bloxstrap -{ - public class Bootstrapper - { - #region Properties - private const int ProgressBarMaximum = 10000; - - private const double TaskbarProgressMaximumWpf = 1; // this can not be changed. keep it at 1. - private const int TaskbarProgressMaximumWinForms = WinFormsDialogBase.TaskbarProgressMaximum; - - private const string AppSettings = - "\r\n" + - "\r\n" + - " content\r\n" + - " http://www.roblox.com\r\n" + - "\r\n"; - - private readonly FastZipEvents _fastZipEvents = new(); - private readonly CancellationTokenSource _cancelTokenSource = new(); - - private readonly IAppData AppData; - private readonly LaunchMode _launchMode; - - private string _launchCommandLine = App.LaunchSettings.RobloxLaunchArgs; - private string _latestVersionGuid = null!; - private string _latestVersionDirectory = null!; - private PackageManifest _versionPackageManifest = null!; - - private bool _isInstalling = false; - private double _progressIncrement; - private double _taskbarProgressIncrement; - private double _taskbarProgressMaximum; - private long _totalDownloadedBytes = 0; - - private bool _mustUpgrade => String.IsNullOrEmpty(AppData.State.VersionGuid) || !File.Exists(AppData.ExecutablePath); - private bool _noConnection = false; - - private AsyncMutex? _mutex; - - private int _appPid = 0; - - public IBootstrapperDialog? Dialog = null; - - public bool IsStudioLaunch => _launchMode != LaunchMode.Player; - #endregion - - #region Core - public Bootstrapper(LaunchMode launchMode) - { - _launchMode = launchMode; - - // 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.DirectoryFailure += (_, e) => throw e.Exception; - _fastZipEvents.ProcessFile += (_, e) => e.ContinueRunning = !_cancelTokenSource.IsCancellationRequested; - - AppData = IsStudioLaunch ? new RobloxStudioData() : new RobloxPlayerData(); - Deployment.BinaryType = AppData.BinaryType; - } - - private void SetStatus(string message) - { - App.Logger.WriteLine("Bootstrapper::SetStatus", message); - - message = message.Replace("{product}", AppData.ProductName); - - if (Dialog is not null) - Dialog.Message = message; - } - - private void UpdateProgressBar() - { - if (Dialog is null) - return; - - // UI progress - int progressValue = (int)Math.Floor(_progressIncrement * _totalDownloadedBytes); - - // bugcheck: if we're restoring a file from a package, it'll incorrectly increment the progress beyond 100 - // too lazy to fix properly so lol - progressValue = Math.Clamp(progressValue, 0, ProgressBarMaximum); - - Dialog.ProgressValue = progressValue; - - // taskbar progress - double taskbarProgressValue = _taskbarProgressIncrement * _totalDownloadedBytes; - taskbarProgressValue = Math.Clamp(taskbarProgressValue, 0, _taskbarProgressMaximum); - - Dialog.TaskbarProgressValue = taskbarProgressValue; - } - - private void HandleConnectionError(Exception exception) - { - const string LOG_IDENT = "Bootstrapper::HandleConnectionError"; - - _noConnection = true; - - App.Logger.WriteLine(LOG_IDENT, "Connectivity check failed"); - App.Logger.WriteException(LOG_IDENT, exception); - - string message = Strings.Dialog_Connectivity_BadConnection; - - if (exception is AggregateException) - exception = exception.InnerException!; - - // https://gist.github.com/pizzaboxer/4b58303589ee5b14cc64397460a8f386 - if (exception is HttpRequestException && exception.InnerException is null) - message = String.Format(Strings.Dialog_Connectivity_RobloxDown, "[status.roblox.com](https://status.roblox.com)"); - - if (_mustUpgrade) - message += $"\n\n{Strings.Dialog_Connectivity_RobloxUpgradeNeeded}\n\n{Strings.Dialog_Connectivity_TryAgainLater}"; - else - message += $"\n\n{Strings.Dialog_Connectivity_RobloxUpgradeSkip}"; - - Frontend.ShowConnectivityDialog( - String.Format(Strings.Dialog_Connectivity_UnableToConnect, "Roblox"), - message, - _mustUpgrade ? MessageBoxImage.Error : MessageBoxImage.Warning, - exception); - - if (_mustUpgrade) - App.Terminate(ErrorCode.ERROR_CANCELLED); - } - - public async Task Run() - { - const string LOG_IDENT = "Bootstrapper::Run"; - - App.Logger.WriteLine(LOG_IDENT, "Running bootstrapper"); - - // this is now always enabled as of v2.8.0 - if (Dialog is not null) - Dialog.CancelEnabled = true; - - SetStatus(Strings.Bootstrapper_Status_Connecting); - - var connectionResult = await Deployment.InitializeConnectivity(); - - App.Logger.WriteLine(LOG_IDENT, "Connectivity check finished"); - - if (connectionResult is not null) - HandleConnectionError(connectionResult); - -#if (!DEBUG || DEBUG_UPDATER) && !QA_BUILD - if (App.Settings.Prop.CheckForUpdates && !App.LaunchSettings.UpgradeFlag.Active) - { - bool updatePresent = await CheckForUpdates(); - - if (updatePresent) - return; - } -#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; - - try - { - 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 - } - - // wait for mutex to be released if it's not yet - await using var mutex = new AsyncMutex(false, "Bloxstrap-Bootstrapper"); - await mutex.AcquireAsync(_cancelTokenSource.Token); - - _mutex = mutex; - - // reload our configs since they've likely changed by now - if (mutexExists) - { - App.Settings.Load(); - App.State.Load(); - } - - if (!_noConnection) - { - try - { - await GetLatestVersionInfo(); - } - catch (Exception ex) - { - HandleConnectionError(ex); - } - } - - if (!_noConnection) - { - if (AppData.State.VersionGuid != _latestVersionGuid || _mustUpgrade) - 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(); - } - - // check registry entries for every launch, just in case the stock bootstrapper changes it back - - if (IsStudioLaunch) - WindowsRegistry.RegisterStudio(); - else - WindowsRegistry.RegisterPlayer(); - - if (_launchMode != LaunchMode.Player) - await mutex.ReleaseAsync(); - - if (!App.LaunchSettings.NoLaunchFlag.Active && !_cancelTokenSource.IsCancellationRequested) - StartRoblox(); - - await mutex.ReleaseAsync(); - - Dialog?.CloseBootstrapper(); - } - - /// - /// Will throw whatever HttpClient can throw - /// - /// - private async Task GetLatestVersionInfo() - { - const string LOG_IDENT = "Bootstrapper::GetLatestVersionInfo"; - - // before we do anything, we need to query our channel - // if it's set in the launch uri, we need to use it and set the registry key for it - // else, check if the registry key for it exists, and use it - - 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-_]+)", - RegexOptions.IgnoreCase | RegexOptions.CultureInvariant - ); - - if (match.Groups.Count == 2) - { - Deployment.Channel = match.Groups[1].Value.ToLowerInvariant(); - } - else if (key.GetValue("www.roblox.com") is string value && !String.IsNullOrEmpty(value)) - { - Deployment.Channel = value.ToLowerInvariant(); - } - - if (String.IsNullOrEmpty(Deployment.Channel)) - Deployment.Channel = Deployment.DefaultChannel; - - App.Logger.WriteLine(LOG_IDENT, $"Got channel as {Deployment.DefaultChannel}"); - - if (!Deployment.IsDefaultChannel) - App.SendStat("robloxChannel", Deployment.Channel); - - 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; - _latestVersionDirectory = Path.Combine(Paths.Versions, _latestVersionGuid); - - string pkgManifestUrl = Deployment.GetLocation($"/{_latestVersionGuid}-rbxPkgManifest.txt"); - var pkgManifestData = await App.HttpClient.GetStringAsync(pkgManifestUrl); - - _versionPackageManifest = new(pkgManifestData); - } - - private void StartRoblox() - { - const string LOG_IDENT = "Bootstrapper::StartRoblox"; - - SetStatus(Strings.Bootstrapper_Status_Starting); - - if (_launchMode == LaunchMode.Player && App.Settings.Prop.ForceRobloxLanguage) - { - var match = Regex.Match(_launchCommandLine, "gameLocale:([a-z_]+)", RegexOptions.CultureInvariant); - - if (match.Groups.Count == 2) - _launchCommandLine = _launchCommandLine.Replace( - "robloxLocale:en_us", - $"robloxLocale:{match.Groups[1].Value}", - StringComparison.OrdinalIgnoreCase); - } - - var startInfo = new ProcessStartInfo() - { - FileName = AppData.ExecutablePath, - Arguments = _launchCommandLine, - WorkingDirectory = AppData.Directory - }; - - if (_launchMode == LaunchMode.Player && ShouldRunAsAdmin()) - { - startInfo.Verb = "runas"; - startInfo.UseShellExecute = true; - } - else if (_launchMode == LaunchMode.StudioAuth) - { - Process.Start(startInfo); - return; - } - - string? logFileName = null; - - string rbxDir = Path.Combine(Paths.LocalAppData, "Roblox"); - if (!Directory.Exists(rbxDir)) - Directory.CreateDirectory(rbxDir); - - string rbxLogDir = Path.Combine(rbxDir, "logs"); - if (!Directory.Exists(rbxLogDir)) - Directory.CreateDirectory(rbxLogDir); - - var logWatcher = new FileSystemWatcher() - { - Path = rbxLogDir, - Filter = "*.log", - EnableRaisingEvents = true - }; - - var logCreatedEvent = new AutoResetEvent(false); - - logWatcher.Created += (_, e) => - { - logWatcher.EnableRaisingEvents = false; - logFileName = e.FullPath; - logCreatedEvent.Set(); - }; - - // v2.2.0 - byfron will trip if we keep a process handle open for over a minute, so we're doing this now - try - { - using var process = Process.Start(startInfo)!; - _appPid = process.Id; - } - catch (Win32Exception ex) when (ex.NativeErrorCode == 1223) - { - // 1223 = ERROR_CANCELLED, gets thrown if a UAC prompt is cancelled - return; - } - catch (Exception) - { - // attempt a reinstall on next launch - File.Delete(AppData.ExecutablePath); - throw; - } - - App.Logger.WriteLine(LOG_IDENT, $"Started Roblox (PID {_appPid}), waiting for log file"); - - logCreatedEvent.WaitOne(TimeSpan.FromSeconds(15)); - - if (String.IsNullOrEmpty(logFileName)) - { - App.Logger.WriteLine(LOG_IDENT, "Unable to identify log file"); - Frontend.ShowPlayerErrorDialog(); - return; - } - else - { - App.Logger.WriteLine(LOG_IDENT, $"Got log file as {logFileName}"); - } - - _mutex?.ReleaseAsync(); - - if (IsStudioLaunch) - return; - - var autoclosePids = new List(); - - // launch custom integrations now - foreach (var integration in App.Settings.Prop.CustomIntegrations) - { - App.Logger.WriteLine(LOG_IDENT, $"Launching custom integration '{integration.Name}' ({integration.Location} {integration.LaunchArgs} - autoclose is {integration.AutoClose})"); - - int pid = 0; - - try - { - var process = Process.Start(new ProcessStartInfo - { - FileName = integration.Location, - Arguments = integration.LaunchArgs.Replace("\r\n", " "), - WorkingDirectory = Path.GetDirectoryName(integration.Location), - UseShellExecute = true - })!; - - pid = process.Id; - } - catch (Exception ex) - { - App.Logger.WriteLine(LOG_IDENT, $"Failed to launch integration '{integration.Name}'!"); - App.Logger.WriteLine(LOG_IDENT, ex.Message); - } - - if (integration.AutoClose && pid != 0) - autoclosePids.Add(pid); - } - - if (App.Settings.Prop.EnableActivityTracking || App.LaunchSettings.TestModeFlag.Active || autoclosePids.Any()) - { - using var ipl = new InterProcessLock("Watcher", TimeSpan.FromSeconds(5)); - - var watcherData = new WatcherData - { - ProcessId = _appPid, - LogFile = logFileName, - AutoclosePids = autoclosePids - }; - - string watcherDataArg = Convert.ToBase64String(Encoding.UTF8.GetBytes(JsonSerializer.Serialize(watcherData))); - - string args = $"-watcher \"{watcherDataArg}\""; - - if (App.LaunchSettings.TestModeFlag.Active) - args += " -testmode"; - - if (ipl.IsAcquired) - Process.Start(Paths.Process, args); - } - - // allow for window to show, since the log is created pretty far beforehand - Thread.Sleep(1000); - } - - private bool ShouldRunAsAdmin() - { - foreach (var root in WindowsRegistry.Roots) - { - using var key = root.OpenSubKey("SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\AppCompatFlags\\Layers"); - - if (key is null) - continue; - - string? flags = (string?)key.GetValue(AppData.ExecutablePath); - - if (flags is not null && flags.Contains("RUNASADMIN", StringComparison.OrdinalIgnoreCase)) - return true; - } - - return false; - } - - public void Cancel() - { - const string LOG_IDENT = "Bootstrapper::Cancel"; - - if (_cancelTokenSource.IsCancellationRequested) - return; - - App.Logger.WriteLine(LOG_IDENT, "Cancelling launch..."); - - _cancelTokenSource.Cancel(); - - if (Dialog is not null) - Dialog.CancelEnabled = false; - - if (_isInstalling) - { - try - { - // clean up install - if (Directory.Exists(_latestVersionDirectory)) - Directory.Delete(_latestVersionDirectory, true); - } - catch (Exception ex) - { - App.Logger.WriteLine(LOG_IDENT, "Could not fully clean up installation!"); - App.Logger.WriteException(LOG_IDENT, ex); - } - } - else if (_appPid != 0) - { - try - { - using var process = Process.GetProcessById(_appPid); - process.Kill(); - } - catch (Exception) { } - } - - Dialog?.CloseBootstrapper(); - - App.SoftTerminate(ErrorCode.ERROR_CANCELLED); - } -#endregion - - #region App Install - private async Task CheckForUpdates() - { - const string LOG_IDENT = "Bootstrapper::CheckForUpdates"; - - // don't update if there's another instance running (likely running in the background) - // i don't like this, but there isn't much better way of doing it /shrug - if (Process.GetProcessesByName(App.ProjectName).Length > 1) - { - App.Logger.WriteLine(LOG_IDENT, $"More than one Bloxstrap instance running, aborting update check"); - return false; - } - - App.Logger.WriteLine(LOG_IDENT, "Checking for updates..."); - -#if !DEBUG_UPDATER - var releaseInfo = await App.GetLatestRelease(); - - if (releaseInfo is null) - return false; - - var versionComparison = Utilities.CompareVersions(App.Version, releaseInfo.TagName); - - // check if we aren't using a deployed build, so we can update to one if a new version comes out - if (App.IsProductionBuild && versionComparison == VersionComparison.Equal || versionComparison == VersionComparison.GreaterThan) - { - App.Logger.WriteLine(LOG_IDENT, "No updates found"); - return false; - } - - if (Dialog is not null) - Dialog.CancelEnabled = false; - - string version = releaseInfo.TagName; -#else - string version = App.Version; -#endif - - SetStatus(Strings.Bootstrapper_Status_UpgradingBloxstrap); - - try - { -#if DEBUG_UPDATER - string downloadLocation = Path.Combine(Paths.TempUpdates, "Bloxstrap.exe"); - - Directory.CreateDirectory(Paths.TempUpdates); - - File.Copy(Paths.Process, downloadLocation, true); -#else - var asset = releaseInfo.Assets![0]; - - string downloadLocation = Path.Combine(Paths.TempUpdates, asset.Name); - - Directory.CreateDirectory(Paths.TempUpdates); - - App.Logger.WriteLine(LOG_IDENT, $"Downloading {releaseInfo.TagName}..."); - - if (!File.Exists(downloadLocation)) - { - var response = await App.HttpClient.GetAsync(asset.BrowserDownloadUrl); - - await using var fileStream = new FileStream(downloadLocation, FileMode.OpenOrCreate, FileAccess.Write); - await response.Content.CopyToAsync(fileStream); - } -#endif - - App.Logger.WriteLine(LOG_IDENT, $"Starting {version}..."); - - ProcessStartInfo startInfo = new() - { - FileName = downloadLocation, - }; - - startInfo.ArgumentList.Add("-upgrade"); - - foreach (string arg in App.LaunchSettings.Args) - startInfo.ArgumentList.Add(arg); - - if (_launchMode == LaunchMode.Player && !startInfo.ArgumentList.Contains("-player")) - startInfo.ArgumentList.Add("-player"); - else if (_launchMode == LaunchMode.Studio && !startInfo.ArgumentList.Contains("-studio")) - startInfo.ArgumentList.Add("-studio"); - - App.Settings.Save(); - - new InterProcessLock("AutoUpdater"); - - Process.Start(startInfo); - - return true; - } - catch (Exception ex) - { - App.Logger.WriteLine(LOG_IDENT, "An exception occurred when running the auto-updater"); - App.Logger.WriteException(LOG_IDENT, ex); - - Frontend.ShowMessageBox( - string.Format(Strings.Bootstrapper_AutoUpdateFailed, version), - MessageBoxImage.Information - ); - - Utilities.ShellExecute(App.ProjectDownloadLink); - } - - return false; - } - #endregion - - #region Roblox Install - private void CleanupVersionsFolder() - { - const string LOG_IDENT = "Bootstrapper::CleanupVersionsFolder"; - - 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) - { - try - { - Directory.Delete(dir, true); - } - catch (Exception ex) - { - App.Logger.WriteLine(LOG_IDENT, $"Failed to delete {dir}"); - App.Logger.WriteException(LOG_IDENT, ex); - } - } - } - } - - private void MigrateCompatibilityFlags() - { - const string LOG_IDENT = "Bootstrapper::MigrateCompatibilityFlags"; - - string oldClientLocation = Path.Combine(Paths.Versions, AppData.State.VersionGuid, AppData.ExecutableName); - string newClientLocation = Path.Combine(_latestVersionDirectory, AppData.ExecutableName); - - // move old compatibility flags for the old location - using RegistryKey appFlagsKey = Registry.CurrentUser.CreateSubKey($"SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\AppCompatFlags\\Layers"); - string? appFlags = appFlagsKey.GetValue(oldClientLocation) as string; - - if (appFlags is not null) - { - App.Logger.WriteLine(LOG_IDENT, $"Migrating app compatibility flags from {oldClientLocation} to {newClientLocation}..."); - appFlagsKey.SetValueSafe(newClientLocation, appFlags); - appFlagsKey.DeleteValueSafe(oldClientLocation); - } - } - - private static void KillRobloxPlayers() - { - const string LOG_IDENT = "Bootstrapper::KillRobloxPlayers"; - - List processes = new List(); - processes.AddRange(Process.GetProcessesByName("RobloxPlayerBeta")); - processes.AddRange(Process.GetProcessesByName("RobloxCrashHandler")); // roblox studio doesnt depend on crash handler being open, so this should be fine - - foreach (Process process in processes) - { - try - { - process.Kill(); - } - catch (Exception ex) - { - App.Logger.WriteLine(LOG_IDENT, $"Failed to close process {process.Id}"); - App.Logger.WriteException(LOG_IDENT, ex); - } - } - } - - private async Task UpgradeRoblox() - { - const string LOG_IDENT = "Bootstrapper::UpgradeRoblox"; - - if (String.IsNullOrEmpty(AppData.State.VersionGuid)) - SetStatus(Strings.Bootstrapper_Status_Installing); - else - SetStatus(Strings.Bootstrapper_Status_Upgrading); - - Directory.CreateDirectory(Paths.Base); - Directory.CreateDirectory(Paths.Downloads); - Directory.CreateDirectory(Paths.Versions); - - _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 - KillRobloxPlayers(); - - // get a fully clean install - if (Directory.Exists(_latestVersionDirectory)) - { - try - { - Directory.Delete(_latestVersionDirectory, true); - } - catch (Exception ex) - { - App.Logger.WriteLine(LOG_IDENT, "Failed to delete the latest version directory"); - App.Logger.WriteException(LOG_IDENT, ex); - } - } - - Directory.CreateDirectory(_latestVersionDirectory); - - var cachedPackageHashes = Directory.GetFiles(Paths.Downloads).Select(x => Path.GetFileName(x)); - - // package manifest states packed size and uncompressed size in exact bytes - int totalSizeRequired = 0; - - // packed size only matters if we don't already have the package cached on disk - totalSizeRequired += _versionPackageManifest.Where(x => !cachedPackageHashes.Contains(x.Signature)).Sum(x => x.PackedSize); - totalSizeRequired += _versionPackageManifest.Sum(x => x.Size); - - if (Filesystem.GetFreeDiskSpace(Paths.Base) < totalSizeRequired) - { - Frontend.ShowMessageBox(Strings.Bootstrapper_NotEnoughSpace, MessageBoxImage.Error); - App.Terminate(ErrorCode.ERROR_INSTALL_FAILURE); - return; - } - - if (Dialog is not null) - { - Dialog.ProgressStyle = ProgressBarStyle.Continuous; - Dialog.TaskbarProgressState = TaskbarItemProgressState.Normal; - - Dialog.ProgressMaximum = ProgressBarMaximum; - - // compute total bytes to download - int totalPackedSize = _versionPackageManifest.Sum(package => package.PackedSize); - _progressIncrement = (double)ProgressBarMaximum / totalPackedSize; - - if (Dialog is WinFormsDialogBase) - _taskbarProgressMaximum = (double)TaskbarProgressMaximumWinForms; - else - _taskbarProgressMaximum = (double)TaskbarProgressMaximumWpf; - - _taskbarProgressIncrement = _taskbarProgressMaximum / (double)totalPackedSize; - } - - var extractionTasks = new List(); - - foreach (var package in _versionPackageManifest) - { - if (_cancelTokenSource.IsCancellationRequested) - return; - - // download all the packages synchronously - await DownloadPackage(package); - - // we'll extract the runtime installer later if we need to - if (package.Name == "WebView2RuntimeInstaller.zip") - continue; - - // extract the package async immediately after download - extractionTasks.Add(Task.Run(() => ExtractPackage(package), _cancelTokenSource.Token)); - } - - if (_cancelTokenSource.IsCancellationRequested) - return; - - if (Dialog is not null) - { - Dialog.ProgressStyle = ProgressBarStyle.Marquee; - Dialog.TaskbarProgressState = TaskbarItemProgressState.Indeterminate; - SetStatus(Strings.Bootstrapper_Status_Configuring); - } - - await Task.WhenAll(extractionTasks); - - App.Logger.WriteLine(LOG_IDENT, "Writing AppSettings.xml..."); - await File.WriteAllTextAsync(Path.Combine(_latestVersionDirectory, "AppSettings.xml"), AppSettings); - - if (_cancelTokenSource.IsCancellationRequested) - return; - - if (App.State.Prop.PromptWebView2Install) - { - using var hklmKey = Registry.LocalMachine.OpenSubKey("SOFTWARE\\WOW6432Node\\Microsoft\\EdgeUpdate\\Clients\\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}"); - using var hkcuKey = Registry.CurrentUser.OpenSubKey("Software\\Microsoft\\EdgeUpdate\\Clients\\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}"); - - if (hklmKey is not null || hkcuKey is not null) - { - // reset prompt state if the user has it installed - App.State.Prop.PromptWebView2Install = true; - } - else - { - var result = Frontend.ShowMessageBox(Strings.Bootstrapper_WebView2NotFound, MessageBoxImage.Warning, MessageBoxButton.YesNo, MessageBoxResult.Yes); - - if (result != MessageBoxResult.Yes) - { - App.State.Prop.PromptWebView2Install = false; - } - else - { - App.Logger.WriteLine(LOG_IDENT, "Installing WebView2 runtime..."); - - var package = _versionPackageManifest.Find(x => x.Name == "WebView2RuntimeInstaller.zip"); - - if (package is null) - { - App.Logger.WriteLine(LOG_IDENT, "Aborted runtime install because package does not exist, has WebView2 been added in this Roblox version yet?"); - return; - } - - string baseDirectory = Path.Combine(_latestVersionDirectory, AppData.PackageDirectoryMap[package.Name]); - - ExtractPackage(package); - - SetStatus(Strings.Bootstrapper_Status_InstallingWebView2); - - var startInfo = new ProcessStartInfo() - { - WorkingDirectory = baseDirectory, - FileName = Path.Combine(baseDirectory, "MicrosoftEdgeWebview2Setup.exe"), - Arguments = "/silent /install" - }; - - await Process.Start(startInfo)!.WaitForExitAsync(); - - App.Logger.WriteLine(LOG_IDENT, "Finished installing runtime"); - - Directory.Delete(baseDirectory, true); - } - } - } - - // finishing and cleanup - - MigrateCompatibilityFlags(); - - AppData.State.VersionGuid = _latestVersionGuid; - - AppData.State.PackageHashes.Clear(); - - foreach (var package in _versionPackageManifest) - AppData.State.PackageHashes.Add(package.Name, package.Signature); - - CleanupVersionsFolder(); - - var allPackageHashes = new List(); - - allPackageHashes.AddRange(App.State.Prop.Player.PackageHashes.Values); - allPackageHashes.AddRange(App.State.Prop.Studio.PackageHashes.Values); - - foreach (string hash in cachedPackageHashes) - { - if (!allPackageHashes.Contains(hash)) - { - 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); - } - } - } - - App.Logger.WriteLine(LOG_IDENT, "Registering approximate program size..."); - - int distributionSize = _versionPackageManifest.Sum(x => x.Size + x.PackedSize) / 1024; - - AppData.State.Size = distributionSize; - - int totalSize = App.State.Prop.Player.Size + App.State.Prop.Studio.Size; - - using (var uninstallKey = Registry.CurrentUser.CreateSubKey(App.UninstallKey)) - { - uninstallKey.SetValueSafe("EstimatedSize", totalSize); - } - - App.Logger.WriteLine(LOG_IDENT, $"Registered as {totalSize} KB"); - - App.State.Save(); - - _isInstalling = false; - } - - private async Task ApplyModifications() - { - const string LOG_IDENT = "Bootstrapper::ApplyModifications"; - - SetStatus(Strings.Bootstrapper_Status_ApplyingModifications); - - // handle file mods - App.Logger.WriteLine(LOG_IDENT, "Checking file mods..."); - - // manifest has been moved to State.json - File.Delete(Path.Combine(Paths.Base, "ModManifest.txt")); - - List modFolderFiles = new(); - - Directory.CreateDirectory(Paths.Modifications); - - // check custom font mod - // instead of replacing the fonts themselves, we'll just alter the font family manifests - - string modFontFamiliesFolder = Path.Combine(Paths.Modifications, "content\\fonts\\families"); - - if (File.Exists(Paths.CustomFont)) - { - App.Logger.WriteLine(LOG_IDENT, "Begin font check"); - - Directory.CreateDirectory(modFontFamiliesFolder); - - const string path = "rbxasset://fonts/CustomFont.ttf"; - - // lets make sure the content/fonts/families path exists in the version directory - string contentFolder = Path.Combine(_latestVersionDirectory, "content"); - Directory.CreateDirectory(contentFolder); - - string fontsFolder = Path.Combine(contentFolder, "fonts"); - Directory.CreateDirectory(fontsFolder); - - string familiesFolder = Path.Combine(fontsFolder, "families"); - Directory.CreateDirectory(familiesFolder); - - foreach (string jsonFilePath in Directory.GetFiles(familiesFolder)) - { - string jsonFilename = Path.GetFileName(jsonFilePath); - string modFilepath = Path.Combine(modFontFamiliesFolder, jsonFilename); - - if (File.Exists(modFilepath)) - continue; - - App.Logger.WriteLine(LOG_IDENT, $"Setting font for {jsonFilename}"); - - var fontFamilyData = JsonSerializer.Deserialize(File.ReadAllText(jsonFilePath)); - - if (fontFamilyData is null) - continue; - - bool shouldWrite = false; - - foreach (var fontFace in fontFamilyData.Faces) - { - if (fontFace.AssetId != path) - { - fontFace.AssetId = path; - shouldWrite = true; - } - } - - if (shouldWrite) - File.WriteAllText(modFilepath, JsonSerializer.Serialize(fontFamilyData, new JsonSerializerOptions { WriteIndented = true })); - } - - App.Logger.WriteLine(LOG_IDENT, "End font check"); - } - else if (Directory.Exists(modFontFamiliesFolder)) - { - Directory.Delete(modFontFamiliesFolder, true); - } - - foreach (string file in Directory.GetFiles(Paths.Modifications, "*.*", SearchOption.AllDirectories)) - { - if (_cancelTokenSource.IsCancellationRequested) - return; - - // get relative directory path - string relativeFile = file.Substring(Paths.Modifications.Length + 1); - - // v1.7.0 - README has been moved to the preferences menu now - if (relativeFile == "README.txt") - { - File.Delete(file); - continue; - } - - if (!App.Settings.Prop.UseFastFlagManager && String.Equals(relativeFile, "ClientSettings\\ClientAppSettings.json", StringComparison.OrdinalIgnoreCase)) - continue; - - if (relativeFile.EndsWith(".lock")) - continue; - - modFolderFiles.Add(relativeFile); - - string fileModFolder = Path.Combine(Paths.Modifications, relativeFile); - string fileVersionFolder = Path.Combine(_latestVersionDirectory, relativeFile); - - if (File.Exists(fileVersionFolder) && MD5Hash.FromFile(fileModFolder) == MD5Hash.FromFile(fileVersionFolder)) - { - App.Logger.WriteLine(LOG_IDENT, $"{relativeFile} already exists in the version folder, and is a match"); - continue; - } - - 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"); - } - - // the manifest is primarily here to keep track of what files have been - // deleted from the modifications folder, so that we know when to restore the original files from the downloaded packages - // now check for files that have been deleted from the mod folder according to the manifest - - var fileRestoreMap = new Dictionary>(); - - foreach (string fileLocation in App.State.Prop.ModManifest) - { - if (modFolderFiles.Contains(fileLocation)) - continue; - - var packageMapEntry = AppData.PackageDirectoryMap.SingleOrDefault(x => !String.IsNullOrEmpty(x.Value) && fileLocation.StartsWith(x.Value)); - string packageName = packageMapEntry.Key; - - // package doesn't exist, likely mistakenly placed file - if (String.IsNullOrEmpty(packageName)) - { - App.Logger.WriteLine(LOG_IDENT, $"{fileLocation} was removed as a mod but does not belong to a package"); - - string versionFileLocation = Path.Combine(_latestVersionDirectory, fileLocation); - - if (File.Exists(versionFileLocation)) - File.Delete(versionFileLocation); - - continue; - } - - string fileName = fileLocation.Substring(packageMapEntry.Value.Length); - - if (!fileRestoreMap.ContainsKey(packageName)) - fileRestoreMap[packageName] = new(); - - fileRestoreMap[packageName].Add(fileName); - - App.Logger.WriteLine(LOG_IDENT, $"{fileLocation} was removed as a mod, restoring from {packageName}"); - } - - foreach (var entry in fileRestoreMap) - { - var package = _versionPackageManifest.Find(x => x.Name == entry.Key); - - if (package is not null) - { - if (_cancelTokenSource.IsCancellationRequested) - return; - - await DownloadPackage(package); - ExtractPackage(package, entry.Value); - } - } - - App.State.Prop.ModManifest = modFolderFiles; - App.State.Save(); - - App.Logger.WriteLine(LOG_IDENT, $"Finished checking file mods"); - } - - private async Task DownloadPackage(Package package) - { - string LOG_IDENT = $"Bootstrapper::DownloadPackage.{package.Name}"; - - if (_cancelTokenSource.IsCancellationRequested) - return; - - Directory.CreateDirectory(Paths.Downloads); - - string packageUrl = Deployment.GetLocation($"/{_latestVersionGuid}-{package.Name}"); - string robloxPackageLocation = Path.Combine(Paths.LocalAppData, "Roblox", "Downloads", package.Signature); - - if (File.Exists(package.DownloadPath)) - { - var file = new FileInfo(package.DownloadPath); - - string calculatedMD5 = MD5Hash.FromFile(package.DownloadPath); - - if (calculatedMD5 != package.Signature) - { - App.Logger.WriteLine(LOG_IDENT, $"Package is corrupted ({calculatedMD5} != {package.Signature})! Deleting and re-downloading..."); - file.Delete(); - } - else - { - App.Logger.WriteLine(LOG_IDENT, $"Package is already downloaded, skipping..."); - - _totalDownloadedBytes += package.PackedSize; - UpdateProgressBar(); - - return; - } - } - else if (File.Exists(robloxPackageLocation)) - { - // let's cheat! if the stock bootstrapper already previously downloaded the file, - // then we can just copy the one from there - - App.Logger.WriteLine(LOG_IDENT, $"Found existing copy at '{robloxPackageLocation}'! Copying to Downloads folder..."); - File.Copy(robloxPackageLocation, package.DownloadPath); - - _totalDownloadedBytes += package.PackedSize; - UpdateProgressBar(); - - return; - } - - if (File.Exists(package.DownloadPath)) - return; - - const int maxTries = 5; - - App.Logger.WriteLine(LOG_IDENT, "Downloading..."); - - var buffer = new byte[4096]; - - for (int i = 1; i <= maxTries; i++) - { - if (_cancelTokenSource.IsCancellationRequested) - return; - - int totalBytesRead = 0; - - try - { - var response = await App.HttpClient.GetAsync(packageUrl, HttpCompletionOption.ResponseHeadersRead, _cancelTokenSource.Token); - await using var stream = await response.Content.ReadAsStreamAsync(_cancelTokenSource.Token); - await using var fileStream = new FileStream(package.DownloadPath, FileMode.CreateNew, FileAccess.ReadWrite, FileShare.Delete); - - while (true) - { - if (_cancelTokenSource.IsCancellationRequested) - { - stream.Close(); - fileStream.Close(); - return; - } - - int bytesRead = await stream.ReadAsync(buffer, _cancelTokenSource.Token); - - if (bytesRead == 0) - break; - - totalBytesRead += bytesRead; - - await fileStream.WriteAsync(buffer.AsMemory(0, bytesRead), _cancelTokenSource.Token); - - _totalDownloadedBytes += bytesRead; - UpdateProgressBar(); - } - - string hash = MD5Hash.FromStream(fileStream); - - if (hash != package.Signature) - throw new ChecksumFailedException($"Failed to verify download of {packageUrl}\n\nExpected hash: {package.Signature}\nGot hash: {hash}"); - - App.Logger.WriteLine(LOG_IDENT, $"Finished downloading! ({totalBytesRead} bytes total)"); - break; - } - catch (Exception ex) - { - App.Logger.WriteLine(LOG_IDENT, $"An exception occurred after downloading {totalBytesRead} bytes. ({i}/{maxTries})"); - App.Logger.WriteException(LOG_IDENT, ex); - - if (ex.GetType() == typeof(ChecksumFailedException)) - { - App.SendStat("packageDownloadState", "httpFail"); - - Frontend.ShowConnectivityDialog( - Strings.Dialog_Connectivity_UnableToDownload, - String.Format(Strings.Dialog_Connectivity_UnableToDownloadReason, "[https://github.com/bloxstraplabs/bloxstrap/wiki/Bloxstrap-is-unable-to-download-Roblox](https://github.com/bloxstraplabs/bloxstrap/wiki/Bloxstrap-is-unable-to-download-Roblox)"), - MessageBoxImage.Error, - ex - ); - - App.Terminate(ErrorCode.ERROR_CANCELLED); - } - else if (i >= maxTries) - throw; - - if (File.Exists(package.DownloadPath)) - File.Delete(package.DownloadPath); - - _totalDownloadedBytes -= totalBytesRead; - UpdateProgressBar(); - - // attempt download over HTTP - // this isn't actually that unsafe - signatures were fetched earlier over HTTPS - // so we've already established that our signatures are legit, and that there's very likely no MITM anyway - if (ex.GetType() == typeof(IOException) && !packageUrl.StartsWith("http://")) - { - App.Logger.WriteLine(LOG_IDENT, "Retrying download over HTTP..."); - packageUrl = packageUrl.Replace("https://", "http://"); - } - } - } - } - - private void ExtractPackage(Package package, List? files = null) - { - const string LOG_IDENT = "Bootstrapper::ExtractPackage"; - - string? packageDir = AppData.PackageDirectoryMap.GetValueOrDefault(package.Name); - - if (packageDir is null) - { - App.Logger.WriteLine(LOG_IDENT, $"WARNING: {package.Name} was not found in the package map!"); - return; - } - - string packageFolder = Path.Combine(_latestVersionDirectory, packageDir); - string? fileFilter = null; - - // for sharpziplib, each file in the filter needs to be a regex - if (files is not null) - { - var regexList = new List(); - - foreach (string file in files) - regexList.Add("^" + file.Replace("\\", "\\\\") + "$"); - - fileFilter = String.Join(';', regexList); - } - - App.Logger.WriteLine(LOG_IDENT, $"Extracting {package.Name}..."); - - var fastZip = new FastZip(_fastZipEvents); - - fastZip.ExtractZip(package.DownloadPath, packageFolder, fileFilter); - - App.Logger.WriteLine(LOG_IDENT, $"Finished extracting {package.Name}"); - } - #endregion - } -} diff --git a/Bloxstrap/Enums/BootstrapperIcon.cs b/Bloxstrap/Enums/BootstrapperIcon.cs deleted file mode 100644 index 4125cc6..0000000 --- a/Bloxstrap/Enums/BootstrapperIcon.cs +++ /dev/null @@ -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 - } -} diff --git a/Bloxstrap/Enums/BootstrapperStyle.cs b/Bloxstrap/Enums/BootstrapperStyle.cs deleted file mode 100644 index 5c5f6fd..0000000 --- a/Bloxstrap/Enums/BootstrapperStyle.cs +++ /dev/null @@ -1,15 +0,0 @@ -namespace Bloxstrap.Enums -{ - public enum BootstrapperStyle - { - VistaDialog, - LegacyDialog2008, - LegacyDialog2011, - ProgressDialog, - ClassicFluentDialog, - ByfronDialog, - [EnumName(StaticName = "Bloxstrap")] - FluentDialog, - FluentAeroDialog - } -} diff --git a/Bloxstrap/Enums/CursorType.cs b/Bloxstrap/Enums/CursorType.cs deleted file mode 100644 index 8b0fa60..0000000 --- a/Bloxstrap/Enums/CursorType.cs +++ /dev/null @@ -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 - } -} diff --git a/Bloxstrap/Enums/EmojiType.cs b/Bloxstrap/Enums/EmojiType.cs deleted file mode 100644 index cdefefd..0000000 --- a/Bloxstrap/Enums/EmojiType.cs +++ /dev/null @@ -1,11 +0,0 @@ -namespace Bloxstrap.Enums -{ - public enum EmojiType - { - Default, - Catmoji, - Windows11, - Windows10, - Windows8 - } -} diff --git a/Bloxstrap/Enums/ErrorCode.cs b/Bloxstrap/Enums/ErrorCode.cs deleted file mode 100644 index 4d830dd..0000000 --- a/Bloxstrap/Enums/ErrorCode.cs +++ /dev/null @@ -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 - } -} diff --git a/Bloxstrap/Enums/FlagPresets/InGameMenuVersion.cs b/Bloxstrap/Enums/FlagPresets/InGameMenuVersion.cs deleted file mode 100644 index 30bbc70..0000000 --- a/Bloxstrap/Enums/FlagPresets/InGameMenuVersion.cs +++ /dev/null @@ -1,12 +0,0 @@ -namespace Bloxstrap.Enums.FlagPresets -{ - public enum InGameMenuVersion - { - [EnumName(FromTranslation = "Common.Default")] - Default, - V1, - V2, - V4, - V4Chrome - } -} diff --git a/Bloxstrap/Enums/FlagPresets/LightingMode.cs b/Bloxstrap/Enums/FlagPresets/LightingMode.cs deleted file mode 100644 index 88ba4da..0000000 --- a/Bloxstrap/Enums/FlagPresets/LightingMode.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace Bloxstrap.Enums.FlagPresets -{ - public enum LightingMode - { - Default, - Voxel, - ShadowMap, - Future - } -} diff --git a/Bloxstrap/Enums/FlagPresets/MSAAMode.cs b/Bloxstrap/Enums/FlagPresets/MSAAMode.cs deleted file mode 100644 index e5ae281..0000000 --- a/Bloxstrap/Enums/FlagPresets/MSAAMode.cs +++ /dev/null @@ -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 - } -} diff --git a/Bloxstrap/Enums/FlagPresets/RenderingMode.cs b/Bloxstrap/Enums/FlagPresets/RenderingMode.cs deleted file mode 100644 index 082d301..0000000 --- a/Bloxstrap/Enums/FlagPresets/RenderingMode.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace Bloxstrap.Enums.FlagPresets -{ - public enum RenderingMode - { - [EnumName(FromTranslation = "Common.Automatic")] - Default, - D3D11, - D3D10, - } -} diff --git a/Bloxstrap/Enums/FlagPresets/TextureQuality.cs b/Bloxstrap/Enums/FlagPresets/TextureQuality.cs deleted file mode 100644 index 9610135..0000000 --- a/Bloxstrap/Enums/FlagPresets/TextureQuality.cs +++ /dev/null @@ -1,12 +0,0 @@ -namespace Bloxstrap.Enums.FlagPresets -{ - public enum TextureQuality - { - [EnumName(FromTranslation = "Common.Automatic")] - Default, - Level0, - Level1, - Level2, - Level3 - } -} diff --git a/Bloxstrap/Enums/GenericTriState.cs b/Bloxstrap/Enums/GenericTriState.cs deleted file mode 100644 index c8b406b..0000000 --- a/Bloxstrap/Enums/GenericTriState.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace Bloxstrap.Enums -{ - public enum GenericTriState - { - Successful, - Failed, - Unknown - } -} diff --git a/Bloxstrap/Enums/LaunchMode.cs b/Bloxstrap/Enums/LaunchMode.cs deleted file mode 100644 index c045d4f..0000000 --- a/Bloxstrap/Enums/LaunchMode.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace Bloxstrap.Enums -{ - public enum LaunchMode - { - None, - Player, - Studio, - StudioAuth - } -} diff --git a/Bloxstrap/Enums/NextAction.cs b/Bloxstrap/Enums/NextAction.cs deleted file mode 100644 index 607029e..0000000 --- a/Bloxstrap/Enums/NextAction.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace Bloxstrap.Enums -{ - public enum NextAction - { - Terminate, - LaunchSettings, - LaunchRoblox, - LaunchRobloxStudio - } -} diff --git a/Bloxstrap/Enums/ServerType.cs b/Bloxstrap/Enums/ServerType.cs deleted file mode 100644 index 70386d6..0000000 --- a/Bloxstrap/Enums/ServerType.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace Bloxstrap.Enums -{ - public enum ServerType - { - Public, - Private, - Reserved - } -} diff --git a/Bloxstrap/Enums/Theme.cs b/Bloxstrap/Enums/Theme.cs deleted file mode 100644 index f3cd71c..0000000 --- a/Bloxstrap/Enums/Theme.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace Bloxstrap.Enums -{ - public enum Theme - { - [EnumName(FromTranslation = "Common.SystemDefault")] - Default, - Light, - Dark - } -} diff --git a/Bloxstrap/Enums/VersionComparison.cs b/Bloxstrap/Enums/VersionComparison.cs deleted file mode 100644 index 8f65958..0000000 --- a/Bloxstrap/Enums/VersionComparison.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace Bloxstrap.Enums -{ - enum VersionComparison - { - LessThan = -1, - Equal = 0, - GreaterThan = 1 - } -} diff --git a/Bloxstrap/Exceptions/AssertionException.cs b/Bloxstrap/Exceptions/AssertionException.cs deleted file mode 100644 index 5555baa..0000000 --- a/Bloxstrap/Exceptions/AssertionException.cs +++ /dev/null @@ -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.") - { - } - } -} diff --git a/Bloxstrap/Exceptions/ChecksumFailedException.cs b/Bloxstrap/Exceptions/ChecksumFailedException.cs deleted file mode 100644 index 95d8af2..0000000 --- a/Bloxstrap/Exceptions/ChecksumFailedException.cs +++ /dev/null @@ -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) - { - } - } -} diff --git a/Bloxstrap/Exceptions/InvalidChannelException.cs b/Bloxstrap/Exceptions/InvalidChannelException.cs deleted file mode 100644 index eff6d79..0000000 --- a/Bloxstrap/Exceptions/InvalidChannelException.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace Bloxstrap.Exceptions -{ - public class InvalidChannelException : Exception - { - public HttpStatusCode? StatusCode; - - public InvalidChannelException(HttpStatusCode? statusCode) : base() - => StatusCode = statusCode; - } -} diff --git a/Bloxstrap/Exceptions/InvalidHTTPResponseException.cs b/Bloxstrap/Exceptions/InvalidHTTPResponseException.cs deleted file mode 100644 index f6b45a0..0000000 --- a/Bloxstrap/Exceptions/InvalidHTTPResponseException.cs +++ /dev/null @@ -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) { } - } -} diff --git a/Bloxstrap/Extensions/BootstrapperIconEx.cs b/Bloxstrap/Extensions/BootstrapperIconEx.cs deleted file mode 100644 index 943cad6..0000000 --- a/Bloxstrap/Extensions/BootstrapperIconEx.cs +++ /dev/null @@ -1,70 +0,0 @@ -using System.Drawing; - -namespace Bloxstrap.Extensions -{ - static class BootstrapperIconEx - { - public static IReadOnlyCollection 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 - }; - } - } -} diff --git a/Bloxstrap/Extensions/BootstrapperStyleEx.cs b/Bloxstrap/Extensions/BootstrapperStyleEx.cs deleted file mode 100644 index 3802264..0000000 --- a/Bloxstrap/Extensions/BootstrapperStyleEx.cs +++ /dev/null @@ -1,19 +0,0 @@ -namespace Bloxstrap.Extensions -{ - static class BootstrapperStyleEx - { - public static IBootstrapperDialog GetNew(this BootstrapperStyle bootstrapperStyle) => Frontend.GetBootstrapperDialog(bootstrapperStyle); - - public static IReadOnlyCollection Selections => new BootstrapperStyle[] - { - BootstrapperStyle.FluentDialog, - BootstrapperStyle.FluentAeroDialog, - BootstrapperStyle.ClassicFluentDialog, - BootstrapperStyle.ByfronDialog, - BootstrapperStyle.ProgressDialog, - BootstrapperStyle.LegacyDialog2011, - BootstrapperStyle.LegacyDialog2008, - BootstrapperStyle.VistaDialog - }; - } -} diff --git a/Bloxstrap/Extensions/DateTimeEx.cs b/Bloxstrap/Extensions/DateTimeEx.cs deleted file mode 100644 index 0dc8a24..0000000 --- a/Bloxstrap/Extensions/DateTimeEx.cs +++ /dev/null @@ -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); - } - } -} diff --git a/Bloxstrap/Extensions/EmojiTypeEx.cs b/Bloxstrap/Extensions/EmojiTypeEx.cs deleted file mode 100644 index e1b32fd..0000000 --- a/Bloxstrap/Extensions/EmojiTypeEx.cs +++ /dev/null @@ -1,31 +0,0 @@ -namespace Bloxstrap.Extensions -{ - static class EmojiTypeEx - { - public static IReadOnlyDictionary Filenames => new Dictionary - { - { EmojiType.Catmoji, "Catmoji.ttf" }, - { EmojiType.Windows11, "Win1122H2SegoeUIEmoji.ttf" }, - { EmojiType.Windows10, "Win10April2018SegoeUIEmoji.ttf" }, - { EmojiType.Windows8, "Win8.1SegoeUIEmoji.ttf" }, - }; - - public static IReadOnlyDictionary Hashes => new Dictionary - { - { 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]}"; - } - } -} diff --git a/Bloxstrap/Extensions/IconEx.cs b/Bloxstrap/Extensions/IconEx.cs deleted file mode 100644 index 516899d..0000000 --- a/Bloxstrap/Extensions/IconEx.cs +++ /dev/null @@ -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); - } - } - } -} diff --git a/Bloxstrap/Extensions/RegistryKeyEx.cs b/Bloxstrap/Extensions/RegistryKeyEx.cs deleted file mode 100644 index 19efd58..0000000 --- a/Bloxstrap/Extensions/RegistryKeyEx.cs +++ /dev/null @@ -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); - } - } - } -} diff --git a/Bloxstrap/Extensions/ResourceManagerEx.cs b/Bloxstrap/Extensions/ResourceManagerEx.cs deleted file mode 100644 index def61c2..0000000 --- a/Bloxstrap/Extensions/ResourceManagerEx.cs +++ /dev/null @@ -1,24 +0,0 @@ -using System.Resources; - -namespace Bloxstrap.Extensions -{ - static class ResourceManagerEx - { - /// - /// Returns the value of the specified string resource.
- /// If the resource is not found, the resource name will be returned. - ///
- public static string GetStringSafe(this ResourceManager manager, string name) => manager.GetStringSafe(name, null); - - /// - /// Returns the value of the string resource localized for the specified culture.
- /// If the resource is not found, the resource name will be returned. - ///
- public static string GetStringSafe(this ResourceManager manager, string name, CultureInfo? culture) - { - string? resourceValue = manager.GetString(name, culture); - - return resourceValue ?? name; - } - } -} diff --git a/Bloxstrap/Extensions/ServerTypeEx.cs b/Bloxstrap/Extensions/ServerTypeEx.cs deleted file mode 100644 index 2b65d8b..0000000 --- a/Bloxstrap/Extensions/ServerTypeEx.cs +++ /dev/null @@ -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, - _ => "?" - }; - } -} diff --git a/Bloxstrap/Extensions/ThemeEx.cs b/Bloxstrap/Extensions/ThemeEx.cs deleted file mode 100644 index f5fc70c..0000000 --- a/Bloxstrap/Extensions/ThemeEx.cs +++ /dev/null @@ -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; - } - } -} diff --git a/Bloxstrap/FastFlagManager.cs b/Bloxstrap/FastFlagManager.cs deleted file mode 100644 index a69b3e5..0000000 --- a/Bloxstrap/FastFlagManager.cs +++ /dev/null @@ -1,263 +0,0 @@ -using Bloxstrap.Enums.FlagPresets; - -namespace Bloxstrap -{ - public class FastFlagManager : JsonManager> - { - 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 PresetFlags = new Dictionary - { - { "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 RenderingModes => new Dictionary - { - { RenderingMode.Default, "None" }, - { RenderingMode.D3D11, "D3D11" }, - { RenderingMode.D3D10, "D3D10" }, - }; - - public static IReadOnlyDictionary LightingModes => new Dictionary - { - { LightingMode.Default, "None" }, - { LightingMode.Voxel, "Voxel" }, - { LightingMode.ShadowMap, "ShadowMap" }, - { LightingMode.Future, "Future" } - }; - - public static IReadOnlyDictionary MSAAModes => new Dictionary - { - { MSAAMode.Default, null }, - { MSAAMode.x1, "1" }, - { MSAAMode.x2, "2" }, - { MSAAMode.x4, "4" } - }; - - public static IReadOnlyDictionary TextureQualityLevels => new Dictionary - { - { 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> IGMenuVersions => new Dictionary> - //{ - // { - // InGameMenuVersion.Default, - // new Dictionary - // { - // { "V2Rollout", null }, - // { "EnableV4", null }, - // { "EnableV4Chrome", null }, - // { "ABTest", null }, - // { "ReportButtonCutOff", null } - // } - // }, - - // { - // InGameMenuVersion.V1, - // new Dictionary - // { - // { "V2Rollout", "0" }, - // { "EnableV4", "False" }, - // { "EnableV4Chrome", "False" }, - // { "ABTest", "False" }, - // { "ReportButtonCutOff", "False" } - // } - // }, - - // { - // InGameMenuVersion.V2, - // new Dictionary - // { - // { "V2Rollout", "100" }, - // { "EnableV4", "False" }, - // { "EnableV4Chrome", "False" }, - // { "ABTest", "False" }, - // { "ReportButtonCutOff", null } - // } - // }, - - // { - // InGameMenuVersion.V4, - // new Dictionary - // { - // { "V2Rollout", "0" }, - // { "EnableV4", "True" }, - // { "EnableV4Chrome", "False" }, - // { "ABTest", "False" }, - // { "ReportButtonCutOff", null } - // } - // }, - - // { - // InGameMenuVersion.V4Chrome, - // new Dictionary - // { - // { "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(IReadOnlyDictionary 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"); - } - } -} diff --git a/Bloxstrap/GlobalCache.cs b/Bloxstrap/GlobalCache.cs deleted file mode 100644 index 6977224..0000000 --- a/Bloxstrap/GlobalCache.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace Bloxstrap -{ - public static class GlobalCache - { - public static readonly Dictionary ServerLocation = new(); - } -} diff --git a/Bloxstrap/GlobalUsings.cs b/Bloxstrap/GlobalUsings.cs deleted file mode 100644 index c04aec2..0000000 --- a/Bloxstrap/GlobalUsings.cs +++ /dev/null @@ -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; \ No newline at end of file diff --git a/Bloxstrap/HttpClientLoggingHandler.cs b/Bloxstrap/HttpClientLoggingHandler.cs deleted file mode 100644 index 3eb830b..0000000 --- a/Bloxstrap/HttpClientLoggingHandler.cs +++ /dev/null @@ -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; - } - } -} diff --git a/Bloxstrap/Installer.cs b/Bloxstrap/Installer.cs deleted file mode 100644 index 93f40c1..0000000 --- a/Bloxstrap/Installer.cs +++ /dev/null @@ -1,624 +0,0 @@ -using System.Windows; -using Microsoft.Win32; - -namespace Bloxstrap -{ - internal class Installer - { - /// - /// Should this version automatically open the release notes page? - /// Recommended for major updates only. - /// - 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(); - - 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 - { - () => - { - 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 - { - () => 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 /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 - ); - } - } - } -} diff --git a/Bloxstrap/Integrations/ActivityWatcher.cs b/Bloxstrap/Integrations/ActivityWatcher.cs deleted file mode 100644 index 8132250..0000000 --- a/Bloxstrap/Integrations/ActivityWatcher.cs +++ /dev/null @@ -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? OnLogEntry; - public event EventHandler? OnGameJoin; - public event EventHandler? OnGameLeave; - public event EventHandler? OnLogOpen; - public event EventHandler? OnAppClose; - public event EventHandler? OnRPCMessage; - - private DateTime LastRPCRequest; - - public string LogLocation = null!; - - public bool InGame = false; - - public ActivityData Data { get; private set; } = new(); - - /// - /// Ordered by newest to oldest - /// - public List 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(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(); - } - 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); - } - } -} diff --git a/Bloxstrap/Integrations/DiscordRichPresence.cs b/Bloxstrap/Integrations/DiscordRichPresence.cs deleted file mode 100644 index 8d140f9..0000000 --- a/Bloxstrap/Integrations/DiscordRichPresence.cs +++ /dev/null @@ -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 _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(); - } - 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 == "") - _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 == "") - _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 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 - - - - - - - - - - - - - - - - - - - - - diff --git a/Bloxstrap/UI/Elements/Bootstrapper/ByfronDialog.xaml.cs b/Bloxstrap/UI/Elements/Bootstrapper/ByfronDialog.xaml.cs deleted file mode 100644 index 5afcc43..0000000 --- a/Bloxstrap/UI/Elements/Bootstrapper/ByfronDialog.xaml.cs +++ /dev/null @@ -1,145 +0,0 @@ -using System.Windows; -using System.ComponentModel; -using System.Windows.Forms; -using System.Windows.Media; -using System.Windows.Media.Imaging; -using System.Windows.Shell; - -using Bloxstrap.UI.Elements.Bootstrapper.Base; -using Bloxstrap.UI.ViewModels.Bootstrapper; - -namespace Bloxstrap.UI.Elements.Bootstrapper -{ - /// - /// Interaction logic for ByfronDialog.xaml - /// - public partial class ByfronDialog : IBootstrapperDialog - { - private readonly ByfronDialogViewModel _viewModel; - - public Bloxstrap.Bootstrapper? Bootstrapper { get; set; } - - private bool _isClosing; - - #region UI Elements - public string Message - { - get => _viewModel.Message; - set - { - string message = value; - if (message.EndsWith("...")) - message = message[..^3]; - - _viewModel.Message = message; - _viewModel.OnPropertyChanged(nameof(_viewModel.Message)); - } - } - - public ProgressBarStyle ProgressStyle - { - get => _viewModel.ProgressIndeterminate ? ProgressBarStyle.Marquee : ProgressBarStyle.Continuous; - set - { - _viewModel.ProgressIndeterminate = (value == ProgressBarStyle.Marquee); - _viewModel.OnPropertyChanged(nameof(_viewModel.ProgressIndeterminate)); - } - } - - public int ProgressMaximum - { - get => _viewModel.ProgressMaximum; - set - { - _viewModel.ProgressMaximum = value; - _viewModel.OnPropertyChanged(nameof(_viewModel.ProgressMaximum)); - } - } - - public int ProgressValue - { - get => _viewModel.ProgressValue; - set - { - _viewModel.ProgressValue = value; - _viewModel.OnPropertyChanged(nameof(_viewModel.ProgressValue)); - } - } - - public TaskbarItemProgressState TaskbarProgressState - { - get => _viewModel.TaskbarProgressState; - set - { - _viewModel.TaskbarProgressState = value; - _viewModel.OnPropertyChanged(nameof(_viewModel.TaskbarProgressState)); - } - } - - public double TaskbarProgressValue - { - get => _viewModel.TaskbarProgressValue; - set - { - _viewModel.TaskbarProgressValue = value; - _viewModel.OnPropertyChanged(nameof(_viewModel.TaskbarProgressValue)); - } - } - - public bool CancelEnabled - { - get => _viewModel.CancelEnabled; - set - { - _viewModel.CancelEnabled = value; - - _viewModel.OnPropertyChanged(nameof(_viewModel.CancelEnabled)); - _viewModel.OnPropertyChanged(nameof(_viewModel.CancelButtonVisibility)); - - _viewModel.OnPropertyChanged(nameof(_viewModel.VersionTextVisibility)); - _viewModel.OnPropertyChanged(nameof(_viewModel.VersionText)); - } - } - #endregion - - public ByfronDialog() - { - string version = Utilities.GetRobloxVersion(Bootstrapper?.IsStudioLaunch ?? false); - _viewModel = new ByfronDialogViewModel(this, version); - DataContext = _viewModel; - Title = App.Settings.Prop.BootstrapperTitle; - Icon = App.Settings.Prop.BootstrapperIcon.GetIcon().GetImageSource(); - - if (App.Settings.Prop.Theme.GetFinal() == Theme.Light) - { - // Matching the roblox website light theme as close as possible. - _viewModel.DialogBorder = new Thickness(1); - _viewModel.Background = new SolidColorBrush(Color.FromRgb(242, 244, 245)); - _viewModel.Foreground = new SolidColorBrush(Color.FromRgb(57, 59, 61)); - _viewModel.IconColor = new SolidColorBrush(Color.FromRgb(57, 59, 61)); - _viewModel.ProgressBarBackground = new SolidColorBrush(Color.FromRgb(189, 190, 190)); - _viewModel.ByfronLogoLocation = new BitmapImage(new Uri("pack://application:,,,/Resources/BootstrapperStyles/ByfronDialog/ByfronLogoLight.jpg")); - } - - InitializeComponent(); - } - private void Window_Closing(object sender, CancelEventArgs e) - { - if (!_isClosing) - Bootstrapper?.Cancel(); - } - - #region IBootstrapperDialog Methods - // Referencing FluentDialog - public void ShowBootstrapper() => this.ShowDialog(); - - public void CloseBootstrapper() - { - _isClosing = true; - Dispatcher.BeginInvoke(this.Close); - } - - public void ShowSuccess(string message, Action? callback) => BaseFunctions.ShowSuccess(message, callback); - #endregion - } -} diff --git a/Bloxstrap/UI/Elements/Bootstrapper/ClassicFluentDialog.xaml b/Bloxstrap/UI/Elements/Bootstrapper/ClassicFluentDialog.xaml deleted file mode 100644 index 76cda5e..0000000 --- a/Bloxstrap/UI/Elements/Bootstrapper/ClassicFluentDialog.xaml +++ /dev/null @@ -1,62 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -