Migrate mod presets to deferred settings system

This commit is contained in:
pizzaboxer 2024-08-16 00:39:08 +01:00
parent de82349e5e
commit 53f77f938e
No known key found for this signature in database
GPG Key ID: 59D4A1DBAD0F2BA8
33 changed files with 550 additions and 317 deletions

View File

@ -1,11 +1,11 @@
using System.Reflection;
using System.Security.Cryptography;
using System.Windows;
using System.Windows.Threading;
using Microsoft.Win32;
using Bloxstrap.Resources;
using Bloxstrap.Models.SettingTasks;
using Bloxstrap.Models.SettingTasks.Base;
namespace Bloxstrap
{
@ -29,11 +29,13 @@ namespace Bloxstrap
public static string Version = Assembly.GetExecutingAssembly().GetName().Version!.ToString()[..^2];
public static readonly MD5 MD5Provider = MD5.Create();
public static NotifyIconWrapper? NotifyIcon { get; set; }
public static readonly Logger Logger = new();
public static readonly Dictionary<string, ISettingTask> PendingSettingTasks = new();
public static readonly Dictionary<string, BaseTask> PendingSettingTasks = new();
public static readonly JsonManager<Settings> Settings = new();

View File

@ -864,72 +864,6 @@ namespace Bloxstrap
if (!Directory.Exists(Paths.Modifications))
Directory.CreateDirectory(Paths.Modifications);
// cursors
await CheckModPreset(App.Settings.Prop.CursorType == CursorType.From2006, new Dictionary<string, string>
{
{ @"content\textures\Cursors\KeyboardMouse\ArrowCursor.png", "Cursor.From2006.ArrowCursor.png" },
{ @"content\textures\Cursors\KeyboardMouse\ArrowFarCursor.png", "Cursor.From2006.ArrowFarCursor.png" }
});
await CheckModPreset(App.Settings.Prop.CursorType == CursorType.From2013, new Dictionary<string, string>
{
{ @"content\textures\Cursors\KeyboardMouse\ArrowCursor.png", "Cursor.From2013.ArrowCursor.png" },
{ @"content\textures\Cursors\KeyboardMouse\ArrowFarCursor.png", "Cursor.From2013.ArrowFarCursor.png" }
});
// character sounds
await CheckModPreset(App.Settings.Prop.UseOldDeathSound, @"content\sounds\ouch.ogg", "Sounds.OldDeath.ogg");
await CheckModPreset(App.Settings.Prop.UseOldCharacterSounds, new Dictionary<string, string>
{
{ @"content\sounds\action_footsteps_plastic.mp3", "Sounds.OldWalk.mp3" },
{ @"content\sounds\action_jump.mp3", "Sounds.OldJump.mp3" },
{ @"content\sounds\action_get_up.mp3", "Sounds.OldGetUp.mp3" },
{ @"content\sounds\action_falling.mp3", "Sounds.Empty.mp3" },
{ @"content\sounds\action_jump_land.mp3", "Sounds.Empty.mp3" },
{ @"content\sounds\action_swim.mp3", "Sounds.Empty.mp3" },
{ @"content\sounds\impact_water.mp3", "Sounds.Empty.mp3" }
});
// Mobile.rbxl
await CheckModPreset(App.Settings.Prop.UseOldAvatarBackground, @"ExtraContent\places\Mobile.rbxl", "OldAvatarBackground.rbxl");
// emoji presets are downloaded remotely from github due to how large they are
string contentFonts = Path.Combine(Paths.Modifications, "content\\fonts");
string emojiFontLocation = Path.Combine(contentFonts, "TwemojiMozilla.ttf");
string emojiFontHash = File.Exists(emojiFontLocation) ? MD5Hash.FromFile(emojiFontLocation) : "";
if (App.Settings.Prop.EmojiType == EmojiType.Default && EmojiTypeEx.Hashes.Values.Contains(emojiFontHash))
{
App.Logger.WriteLine(LOG_IDENT, "Reverting to default emoji font");
File.Delete(emojiFontLocation);
}
else if (App.Settings.Prop.EmojiType != EmojiType.Default && emojiFontHash != App.Settings.Prop.EmojiType.GetHash())
{
App.Logger.WriteLine(LOG_IDENT, $"Configuring emoji font as {App.Settings.Prop.EmojiType}");
if (emojiFontHash != "")
File.Delete(emojiFontLocation);
Directory.CreateDirectory(contentFonts);
try
{
var response = await App.HttpClient.GetAsync(App.Settings.Prop.EmojiType.GetUrl());
response.EnsureSuccessStatusCode();
await using var fileStream = new FileStream(emojiFontLocation, FileMode.CreateNew);
await response.Content.CopyToAsync(fileStream);
}
catch (HttpRequestException ex)
{
App.Logger.WriteLine(LOG_IDENT, $"Failed to fetch emoji preset from Github");
App.Logger.WriteException(LOG_IDENT, ex);
Frontend.ShowMessageBox(string.Format(Strings.Bootstrapper_EmojiPresetFetchFailed, App.Settings.Prop.EmojiType), MessageBoxImage.Warning);
App.Settings.Prop.EmojiType = EmojiType.Default;
}
}
// check custom font mod
// instead of replacing the fonts themselves, we'll just alter the font family manifests
@ -1043,54 +977,6 @@ namespace Bloxstrap
App.Logger.WriteLine(LOG_IDENT, $"Finished checking file mods");
}
private static async Task CheckModPreset(bool condition, string location, string name)
{
string LOG_IDENT = $"Bootstrapper::CheckModPreset.{name}";
string fullLocation = Path.Combine(Paths.Modifications, location);
string fileHash = File.Exists(fullLocation) ? MD5Hash.FromFile(fullLocation) : "";
if (!condition && fileHash == "")
return;
byte[] embeddedData = string.IsNullOrEmpty(name) ? Array.Empty<byte>() : await Resource.Get(name);
string embeddedHash = MD5Hash.FromBytes(embeddedData);
if (!condition)
{
if (fileHash == embeddedHash)
{
App.Logger.WriteLine(LOG_IDENT, $"Deleting '{location}' as preset is disabled, and mod file matches preset");
Filesystem.AssertReadOnly(fullLocation);
File.Delete(fullLocation);
}
return;
}
if (fileHash != embeddedHash)
{
App.Logger.WriteLine(LOG_IDENT, $"Writing '{location}' as preset is enabled, and mod file does not exist or does not match preset");
Directory.CreateDirectory(Path.GetDirectoryName(fullLocation)!);
if (File.Exists(fullLocation))
{
Filesystem.AssertReadOnly(fullLocation);
File.Delete(fullLocation);
}
await File.WriteAllBytesAsync(fullLocation, embeddedData);
}
}
private static async Task CheckModPreset(bool condition, Dictionary<string, string> mapping)
{
foreach (var pair in mapping)
await CheckModPreset(condition, pair.Key, pair.Value);
}
private async Task DownloadPackage(Package package)
{
string LOG_IDENT = $"Bootstrapper::DownloadPackage.{package.Name}";

View File

@ -2,9 +2,14 @@
{
public enum CursorType
{
[EnumSort(Order = 1)]
[EnumName(FromTranslation = "Common.Default")]
Default,
[EnumSort(Order = 3)]
From2006,
[EnumSort(Order = 2)]
From2013
}
}

View File

@ -1,12 +0,0 @@
namespace Bloxstrap.Extensions
{
static class CursorTypeEx
{
public static IReadOnlyCollection<CursorType> Selections => new CursorType[]
{
CursorType.Default,
CursorType.From2013,
CursorType.From2006
};
}
}

View File

@ -2,15 +2,6 @@
{
static class EmojiTypeEx
{
public static IReadOnlyCollection<EmojiType> Selections => new EmojiType[]
{
EmojiType.Default,
EmojiType.Catmoji,
EmojiType.Windows11,
EmojiType.Windows10,
EmojiType.Windows8
};
public static IReadOnlyDictionary<EmojiType, string> Filenames => new Dictionary<EmojiType, string>
{
{ EmojiType.Catmoji, "Catmoji.ttf" },
@ -34,7 +25,7 @@
if (emojiType == EmojiType.Default)
return "";
return $"https://github.com/bloxstraplabs/rbxcustom-fontemojis/releases/download/my-phone-is-78-percent/{Filenames[emojiType]}";
return $"https://github.com/bloxstraplabs/rbxcustom-fontemoji/releases/download/my-phone-is-78-percent/{Filenames[emojiType]}";
}
}
}

View File

@ -1,8 +1,4 @@
using Bloxstrap.Enums.FlagPresets;
using System.Windows.Forms;
using Windows.Win32;
using Windows.Win32.Graphics.Gdi;
namespace Bloxstrap
{

View File

@ -22,5 +22,6 @@ global using Bloxstrap.Models.Attributes;
global using Bloxstrap.Models.BloxstrapRPC;
global using Bloxstrap.Models.RobloxApi;
global using Bloxstrap.Models.Manifest;
global using Bloxstrap.Resources;
global using Bloxstrap.UI;
global using Bloxstrap.Utility;

View File

@ -2,9 +2,10 @@
namespace Bloxstrap
{
public class JsonManager<T> where T : new()
public class JsonManager<T> where T : class, new()
{
public T Prop { get; set; } = new();
public virtual string FileLocation => Path.Combine(Paths.Base, $"{typeof(T).Name}.json");
private string LOG_IDENT_CLASS => $"JsonManager<{typeof(T).Name}>";

View File

@ -0,0 +1,13 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Bloxstrap.Models.Attributes
{
class EnumSortAttribute : Attribute
{
public int Order { get; set; }
}
}

View File

@ -0,0 +1,40 @@
using System.Security.Cryptography;
using System.Windows.Markup;
namespace Bloxstrap.Models
{
public class ModPresetFileData
{
public string FilePath { get; private set; }
public string FullFilePath => Path.Combine(Paths.Modifications, FilePath);
public FileStream FileStream => File.OpenRead(FullFilePath);
public string ResourceIdentifier { get; private set; }
public Stream ResourceStream => Resource.GetStream(ResourceIdentifier);
public byte[] ResourceHash { get; private set; }
public ModPresetFileData(string contentPath, string resource)
{
FilePath = contentPath;
ResourceIdentifier = resource;
using var stream = ResourceStream;
ResourceHash = App.MD5Provider.ComputeHash(stream);
}
public bool HashMatches()
{
if (!File.Exists(FullFilePath))
return false;
using var fileStream = FileStream;
var fileHash = App.MD5Provider.ComputeHash(fileStream);
return fileHash.SequenceEqual(ResourceHash);
}
}
}

View File

@ -0,0 +1,21 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Bloxstrap.Models.SettingTasks.Base
{
public abstract class BaseTask
{
public string Name { get; private set; }
public abstract bool Changed { get; }
public BaseTask(string prefix, string name) => Name = $"{prefix}.{name}";
public override string ToString() => Name;
public abstract void Execute();
}
}

View File

@ -4,36 +4,28 @@ using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Bloxstrap.Models.SettingTasks
namespace Bloxstrap.Models.SettingTasks.Base
{
public class BaseTask : ISettingTask
public abstract class BoolBaseTask : BaseTask
{
private bool _originalState;
private bool _newState;
public string Name { get; set; } = "";
public bool OriginalState
public virtual bool OriginalState
{
get
{
return _originalState;
}
get => _originalState;
set
set
{
_originalState = value;
_newState = value;
}
}
public bool NewState
public virtual bool NewState
{
get
{
return _newState;
}
get => _newState;
set
{
@ -42,6 +34,8 @@ namespace Bloxstrap.Models.SettingTasks
}
}
public virtual void Execute() => throw new NotImplementedException();
public override bool Changed => NewState != OriginalState;
public BoolBaseTask(string prefix, string name) : base(prefix, name) { }
}
}

View File

@ -0,0 +1,49 @@
namespace Bloxstrap.Models.SettingTasks.Base
{
public abstract class EnumBaseTask<T> : BaseTask where T : struct, Enum
{
private T _originalState = default!;
private T _newState = default!;
public virtual T OriginalState
{
get => _originalState;
set
{
_originalState = value;
_newState = value;
}
}
public virtual T NewState
{
get => _newState;
set
{
App.PendingSettingTasks[Name] = this;
_newState = value;
}
}
public override bool Changed => !NewState.Equals(OriginalState);
public IEnumerable<T> Selections { get; private set; }
= Enum.GetValues(typeof(T)).Cast<T>().OrderBy(x =>
{
var attributes = x.GetType().GetMember(x.ToString())[0].GetCustomAttributes(typeof(EnumSortAttribute), false);
if (attributes.Length > 0)
{
var attribute = (EnumSortAttribute)attributes[0];
return attribute.Order;
}
return 0;
});
public EnumBaseTask(string prefix, string name) : base(prefix, name) { }
}
}

View File

@ -0,0 +1,41 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Bloxstrap.Models.SettingTasks.Base
{
public abstract class StringBaseTask : BaseTask
{
private string _originalState = "";
private string _newState = "";
public virtual string OriginalState
{
get => _originalState;
set
{
_originalState = value;
_newState = value;
}
}
public virtual string NewState
{
get => _newState;
set
{
App.PendingSettingTasks[Name] = this;
_newState = value;
}
}
public override bool Changed => NewState != OriginalState;
public StringBaseTask(string prefix, string name) : base(prefix, name) { }
}
}

View File

@ -0,0 +1,69 @@
using System.Windows;
using Bloxstrap.Models.SettingTasks.Base;
namespace Bloxstrap.Models.SettingTasks
{
public class EmojiModPresetTask : EnumBaseTask<EmojiType>
{
private string _filePath => Path.Combine(Paths.Modifications, @"content\fonts\TwemojiMozilla.ttf");
private IEnumerable<KeyValuePair<EmojiType, string>>? QueryCurrentValue()
{
if (!File.Exists(_filePath))
return null;
using var fileStream = File.OpenRead(_filePath);
string hash = MD5Hash.Stringify(App.MD5Provider.ComputeHash(fileStream));
return EmojiTypeEx.Hashes.Where(x => x.Value == hash);
}
public EmojiModPresetTask() : base("ModPreset", "EmojiFont")
{
var query = QueryCurrentValue();
if (query is not null)
OriginalState = query.FirstOrDefault().Key;
}
public override async void Execute()
{
const string LOG_IDENT = "EmojiModPresetTask::Execute";
var query = QueryCurrentValue();
if (NewState != EmojiType.Default && (query is null || query.FirstOrDefault().Key != NewState))
{
try
{
var response = await App.HttpClient.GetAsync(NewState.GetUrl());
response.EnsureSuccessStatusCode();
Directory.CreateDirectory(Path.GetDirectoryName(_filePath)!);
await using var fileStream = new FileStream(_filePath, FileMode.CreateNew);
await response.Content.CopyToAsync(fileStream);
OriginalState = NewState;
}
catch (Exception ex)
{
App.Logger.WriteException(LOG_IDENT, ex);
Frontend.ShowMessageBox(
String.Format(Strings.Menu_Mods_Presets_EmojiType_Error, ex.Message),
MessageBoxImage.Warning);
}
}
else if (query is not null && query.Any())
{
Filesystem.AssertReadOnly(_filePath);
File.Delete(_filePath);
OriginalState = NewState;
}
}
}
}

View File

@ -0,0 +1,68 @@
using Bloxstrap.Models.SettingTasks.Base;
namespace Bloxstrap.Models.SettingTasks
{
public class EnumModPresetTask<T> : EnumBaseTask<T> where T : struct, Enum
{
private readonly Dictionary<T, Dictionary<string, ModPresetFileData>> _fileDataMap = new();
private readonly Dictionary<T, Dictionary<string, string>> _map;
public EnumModPresetTask(string name, Dictionary<T, Dictionary<string, string>> map) : base("ModPreset", name)
{
_map = map;
foreach (var enumPair in _map)
{
var dataMap = new Dictionary<string, ModPresetFileData>();
foreach (var resourcePair in enumPair.Value)
{
var data = new ModPresetFileData(resourcePair.Key, resourcePair.Value);
if (data.HashMatches() && OriginalState.Equals(default(T)))
OriginalState = enumPair.Key;
dataMap[resourcePair.Key] = data;
}
_fileDataMap[enumPair.Key] = dataMap;
}
}
public override void Execute()
{
if (!NewState.Equals(default(T)))
{
var resourceMap = _fileDataMap[NewState];
foreach (var resourcePair in resourceMap)
{
var data = resourcePair.Value;
if (!data.HashMatches())
{
Directory.CreateDirectory(Path.GetDirectoryName(data.FullFilePath)!);
using var resourceStream = data.ResourceStream;
using var memoryStream = new MemoryStream();
data.ResourceStream.CopyTo(memoryStream);
Filesystem.AssertReadOnly(data.FullFilePath);
File.WriteAllBytes(data.FullFilePath, memoryStream.ToArray());
}
}
}
else
{
foreach (var dataPair in _fileDataMap.First().Value)
{
Filesystem.AssertReadOnly(dataPair.Value.FullFilePath);
File.Delete(dataPair.Value.FullFilePath);
}
}
OriginalState = NewState;
}
}
}

View File

@ -0,0 +1,43 @@
using Bloxstrap.Models.SettingTasks.Base;
namespace Bloxstrap.Models.SettingTasks
{
public class FontModPresetTask : StringBaseTask
{
public string? GetFileHash()
{
if (!File.Exists(Paths.CustomFont))
return null;
using var fileStream = File.OpenRead(Paths.CustomFont);
return MD5Hash.Stringify(App.MD5Provider.ComputeHash(fileStream));
}
public FontModPresetTask() : base("ModPreset", "TextFont")
{
if (File.Exists(Paths.CustomFont))
OriginalState = Paths.CustomFont;
}
public override void Execute()
{
if (!String.IsNullOrEmpty(NewState))
{
if (String.Compare(NewState, Paths.CustomFont, StringComparison.InvariantCultureIgnoreCase) != 0 && File.Exists(NewState))
{
Directory.CreateDirectory(Path.GetDirectoryName(Paths.CustomFont)!);
Filesystem.AssertReadOnly(Paths.CustomFont);
File.Copy(NewState, Paths.CustomFont, true);
}
}
else if (File.Exists(Paths.CustomFont))
{
Filesystem.AssertReadOnly(Paths.CustomFont);
File.Delete(Paths.CustomFont);
}
OriginalState = NewState;
}
}
}

View File

@ -1,17 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Bloxstrap.Models.SettingTasks
{
public interface ISettingTask
{
public bool OriginalState { get; set; }
public bool NewState { get; set; }
public void Execute();
}
}

View File

@ -0,0 +1,59 @@
using Bloxstrap.Models.SettingTasks.Base;
namespace Bloxstrap.Models.SettingTasks
{
public class ModPresetTask : BoolBaseTask
{
private Dictionary<string, ModPresetFileData> _fileDataMap = new();
private Dictionary<string, string> _pathMap;
public ModPresetTask(string name, string path, string resource) : this(name, new() {{ path, resource }}) { }
public ModPresetTask(string name, Dictionary<string, string> pathMap) : base("ModPreset", name)
{
_pathMap = pathMap;
foreach (var pair in _pathMap)
{
var data = new ModPresetFileData(pair.Key, pair.Value);
if (data.HashMatches() && !OriginalState)
OriginalState = true;
_fileDataMap[pair.Key] = data;
}
}
public override void Execute()
{
if (NewState == OriginalState)
return;
foreach (var pair in _fileDataMap)
{
var data = pair.Value;
bool hashMatches = data.HashMatches();
if (NewState && !hashMatches)
{
Directory.CreateDirectory(Path.GetDirectoryName(data.FullFilePath)!);
using var resourceStream = data.ResourceStream;
using var memoryStream = new MemoryStream();
data.ResourceStream.CopyTo(memoryStream);
Filesystem.AssertReadOnly(data.FullFilePath);
File.WriteAllBytes(data.FullFilePath, memoryStream.ToArray());
}
else if (!NewState && hashMatches)
{
Filesystem.AssertReadOnly(data.FullFilePath);
File.Delete(data.FullFilePath);
}
}
OriginalState = NewState;
}
}
}

View File

@ -1,33 +1,27 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Bloxstrap.Models.SettingTasks.Base;
namespace Bloxstrap.Models.SettingTasks
{
public class ShortcutTask : BaseTask, ISettingTask
public class ShortcutTask : BoolBaseTask
{
public string ExeFlags { get; set; } = "";
private string _shortcutPath;
private string _exeFlags;
public string ShortcutPath { get; set; }
public ShortcutTask(string shortcutPath)
public ShortcutTask(string name, string lnkFolder, string lnkName, string exeFlags = "") : base("Shortcut", name)
{
ShortcutPath = shortcutPath;
_shortcutPath = Path.Combine(lnkFolder, lnkName);
_exeFlags = exeFlags;
OriginalState = File.Exists(ShortcutPath);
OriginalState = File.Exists(_shortcutPath);
}
public override void Execute()
{
if (NewState == OriginalState)
return;
if (NewState)
Shortcut.Create(Paths.Application, ExeFlags, ShortcutPath);
else if (File.Exists(ShortcutPath))
File.Delete(ShortcutPath);
Shortcut.Create(Paths.Application, _exeFlags, _shortcutPath);
else if (File.Exists(_shortcutPath))
File.Delete(_shortcutPath);
OriginalState = NewState;
}

View File

@ -27,12 +27,7 @@ namespace Bloxstrap.Models
public ObservableCollection<CustomIntegration> CustomIntegrations { get; set; } = new();
// mod preset configuration
public bool UseOldDeathSound { get; set; } = true;
public bool UseOldCharacterSounds { get; set; } = false;
public bool UseDisableAppPatch { get; set; } = false;
public bool UseOldAvatarBackground { get; set; } = false;
public CursorType CursorType { get; set; } = CursorType.Default;
public EmojiType EmojiType { get; set; } = EmojiType.Default;
public bool DisableFullscreenOptimizations { get; set; } = false;
}
}

View File

@ -7,18 +7,19 @@ namespace Bloxstrap
static readonly Assembly assembly = Assembly.GetExecutingAssembly();
static readonly string[] resourceNames = assembly.GetManifestResourceNames();
public static async Task<byte[]> Get(string name)
public static Stream GetStream(string name)
{
string path = resourceNames.Single(str => str.EndsWith(name));
return assembly.GetManifestResourceStream(path)!;
}
using (Stream stream = assembly.GetManifestResourceStream(path)!)
{
using (MemoryStream memoryStream = new())
{
await stream.CopyToAsync(memoryStream);
return memoryStream.ToArray();
}
}
public static async Task<byte[]> Get(string name)
{
using var stream = GetStream(name);
using var memoryStream = new MemoryStream();
await stream.CopyToAsync(memoryStream);
return memoryStream.ToArray();
}
}
}

View File

@ -2733,6 +2733,17 @@ namespace Bloxstrap.Resources {
}
}
/// <summary>
/// Looks up a localized string similar to The emoji mod could not be applied because of a network error during download.
///
///{0}.
/// </summary>
public static string Menu_Mods_Presets_EmojiType_Error {
get {
return ResourceManager.GetString("Menu.Mods.Presets.EmojiType.Error", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Preferred emoji type.
/// </summary>

View File

@ -1119,4 +1119,9 @@ If not, then please report this exception to the maintainers of this fork. Do NO
<data name="ContextMenu.ServerInformation.Notification.Title.Reserved" xml:space="preserve">
<value>Connected to reserved server</value>
</data>
<data name="Menu.Mods.Presets.EmojiType.Error" xml:space="preserve">
<value>The emoji mod could not be applied because of a network error during download.
{0}</value>
</data>
</root>

View File

@ -91,7 +91,7 @@
</ItemsPanelTemplate>
</StatusBar.ItemsPanel>
<StatusBarItem Grid.Column="1" Padding="0,0,4,0">
<ui:Button Content="{x:Static resources:Strings.Menu_Save}" Appearance="Primary" Command="{Binding SaveSettingsCommand, Mode=OneWay}" IsEnabled="{Binding ConfirmButtonEnabled, Mode=OneWay}" />
<ui:Button Content="{x:Static resources:Strings.Menu_Save}" Appearance="Primary" Command="{Binding SaveSettingsCommand, Mode=OneWay}" />
</StatusBarItem>
<StatusBarItem Grid.Column="2" Padding="4,0,0,0">
<ui:Button Content="{x:Static resources:Strings.Common_Close}" IsCancel="True" />

View File

@ -599,7 +599,7 @@
<controls:MarkdownTextBlock MarkdownText="[MaximumADHD](https://github.com/MaximumADHD)" />
<controls:MarkdownTextBlock MarkdownText="[Multako](https://www.roblox.com/users/2485612194/profile)" />
<controls:MarkdownTextBlock MarkdownText="[axstin](https://github.com/axstin)" />
<controls:MarkdownTextBlock MarkdownText="[taskmanager](https://github.com/Manataraix)" />
<controls:MarkdownTextBlock MarkdownText="[taskmanager](https://github.com/Mantaraix)" />
<controls:MarkdownTextBlock MarkdownText="[apprehensions](https://github.com/apprehensions)" />
</StackPanel>
</controls:Expander>

View File

@ -7,7 +7,9 @@
xmlns:ui="http://schemas.lepo.co/wpfui/2022/xaml"
xmlns:resources="clr-namespace:Bloxstrap.Resources"
xmlns:controls="clr-namespace:Bloxstrap.UI.Elements.Controls"
xmlns:dmodels="clr-namespace:Bloxstrap.UI.ViewModels.Settings"
mc:Ignorable="d"
d:DataContext="{d:DesignInstance dmodels:ModsViewModel, IsDesignTimeCreatable=True}"
d:DesignHeight="800" d:DesignWidth="800"
Title="ModsPage"
Scrollable="True">
@ -42,13 +44,13 @@
<controls:OptionControl
Header="{x:Static resources:Strings.Menu_Mods_Presets_OldDeathSound_Title}"
Description="{x:Static resources:Strings.Menu_Mods_Presets_OldDeathSound_Description}">
<ui:ToggleSwitch IsChecked="{Binding OldDeathSoundEnabled, Mode=TwoWay}" />
<ui:ToggleSwitch IsChecked="{Binding OldDeathSoundTask.NewState, Mode=TwoWay}" />
</controls:OptionControl>
<controls:OptionControl
Header="{x:Static resources:Strings.Menu_Mods_Presets_MouseCursor_Title}"
Description="{x:Static resources:Strings.Menu_Mods_Presets_MouseCursor_Description}">
<ComboBox Margin="5,0,0,0" Padding="10,5,10,5" Width="200" ItemsSource="{Binding CursorTypes, Mode=OneTime}" Text="{Binding SelectedCursorType, Mode=TwoWay}">
<ComboBox Margin="5,0,0,0" Padding="10,5,10,5" Width="200" ItemsSource="{Binding CursorTypeTask.Selections, Mode=OneTime}" Text="{Binding CursorTypeTask.NewState, Mode=TwoWay}">
<ComboBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Path=., Converter={StaticResource EnumNameConverter}}" />
@ -60,19 +62,19 @@
<controls:OptionControl
Header="{x:Static resources:Strings.Menu_Mods_Presets_OldAvatarEditor_Title}"
Description="{x:Static resources:Strings.Menu_Mods_Presets_OldAvatarEditor_Description}">
<ui:ToggleSwitch IsChecked="{Binding OldAvatarBackground, Mode=TwoWay}" />
<ui:ToggleSwitch IsChecked="{Binding OldAvatarBackgroundTask.NewState, Mode=TwoWay}" />
</controls:OptionControl>
<controls:OptionControl
Header="{x:Static resources:Strings.Menu_Mods_Presets_OldCharacterSounds_Title}"
Description="{x:Static resources:Strings.Menu_Mods_Presets_OldCharacterSounds_Description}">
<ui:ToggleSwitch IsChecked="{Binding OldCharacterSoundsEnabled, Mode=TwoWay}" />
<ui:ToggleSwitch IsChecked="{Binding OldCharacterSoundsTask.NewState, Mode=TwoWay}" />
</controls:OptionControl>
<controls:OptionControl
Header="{x:Static resources:Strings.Menu_Mods_Presets_EmojiType_Title}"
Description="{x:Static resources:Strings.Menu_Mods_Presets_EmojiType_Description}">
<ComboBox Margin="5,0,0,0" Padding="10,5,10,5" Width="220" ItemsSource="{Binding EmojiTypes, Mode=OneTime}" Text="{Binding SelectedEmojiType, Mode=TwoWay}">
<ComboBox Margin="5,0,0,0" Padding="10,5,10,5" Width="220" ItemsSource="{Binding EmojiFontTask.Selections, Mode=OneTime}" Text="{Binding EmojiFontTask.NewState, Mode=TwoWay}">
<ComboBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Path=., Converter={StaticResource EnumNameConverter}}" />

View File

@ -6,9 +6,10 @@
xmlns:ui="http://schemas.lepo.co/wpfui/2022/xaml"
xmlns:local="clr-namespace:Bloxstrap.UI.Elements.Settings.Pages"
xmlns:controls="clr-namespace:Bloxstrap.UI.Elements.Controls"
xmlns:models="clr-namespace:Bloxstrap.UI.ViewModels.Settings"
xmlns:dmodels="clr-namespace:Bloxstrap.UI.ViewModels.Settings"
xmlns:resources="clr-namespace:Bloxstrap.Resources"
mc:Ignorable="d"
d:DataContext="{d:DesignInstance dmodels:ShortcutsViewModel, IsDesignTimeCreatable=True}"
d:DesignHeight="600" d:DesignWidth="800"
Title="ShortcutsPage"
Scrollable="True">
@ -26,11 +27,11 @@
</Grid.ColumnDefinitions>
<controls:OptionControl Grid.Column="0" Margin="0,0,4,0" Header="{x:Static resources:Strings.Common_Shortcuts_Desktop}">
<ui:ToggleSwitch IsChecked="{Binding DesktopIcon, Mode=TwoWay}" />
<ui:ToggleSwitch IsChecked="{Binding DesktopIconTask.NewState, Mode=TwoWay}" />
</controls:OptionControl>
<controls:OptionControl Grid.Column="1" Margin="4,0,0,0" Header="{x:Static resources:Strings.Common_Shortcuts_StartMenu}">
<ui:ToggleSwitch IsChecked="{Binding StartMenuIcon, Mode=TwoWay}" />
<ui:ToggleSwitch IsChecked="{Binding StartMenuIconTask.NewState, Mode=TwoWay}" />
</controls:OptionControl>
</Grid>
@ -44,11 +45,11 @@
</Grid.ColumnDefinitions>
<controls:OptionControl Grid.Column="0" Margin="0,0,4,0" Header="{x:Static resources:Strings.LaunchMenu_LaunchRoblox}">
<ui:ToggleSwitch IsChecked="{Binding PlayerIcon, Mode=TwoWay}" />
<ui:ToggleSwitch IsChecked="{Binding PlayerIconTask.NewState, Mode=TwoWay}" />
</controls:OptionControl>
<controls:OptionControl Grid.Column="1" Margin="4,0,0,0" Header="{x:Static resources:Strings.Menu_Title}">
<ui:ToggleSwitch IsChecked="{Binding SettingsIcon, Mode=TwoWay}" />
<ui:ToggleSwitch IsChecked="{Binding SettingsIconTask.NewState, Mode=TwoWay}" />
</controls:OptionControl>
</Grid>
</StackPanel>

View File

@ -89,14 +89,15 @@ namespace Bloxstrap.UI
string serverLocation = await _activityWatcher!.GetServerLocation();
string title = _activityWatcher.ActivityServerType switch
{
ServerType.Public => Resources.Strings.ContextMenu_ServerInformation_Notification_Title_Public,
ServerType.Private => Resources.Strings.ContextMenu_ServerInformation_Notification_Title_Private,
ServerType.Reserved => Resources.Strings.ContextMenu_ServerInformation_Notification_Title_Reserved
ServerType.Public => Strings.ContextMenu_ServerInformation_Notification_Title_Public,
ServerType.Private => Strings.ContextMenu_ServerInformation_Notification_Title_Private,
ServerType.Reserved => Strings.ContextMenu_ServerInformation_Notification_Title_Reserved,
_ => ""
};
ShowAlert(
title,
String.Format(Resources.Strings.ContextMenu_ServerInformation_Notification_Text, serverLocation),
String.Format(Strings.ContextMenu_ServerInformation_Notification_Text, serverLocation),
10,
(_, _) => _menuContainer?.ShowServerInformationWindow()
);

View File

@ -12,12 +12,22 @@ namespace Bloxstrap.UI.ViewModels.Settings
private void SaveSettings()
{
const string LOG_IDENT = "MainWindowViewModel::SaveSettings";
App.Settings.Save();
App.State.Save();
App.FastFlags.Save();
foreach (var task in App.PendingSettingTasks)
task.Value.Execute();
foreach (var pair in App.PendingSettingTasks)
{
var task = pair.Value;
if (task.Changed)
{
App.Logger.WriteLine(LOG_IDENT, $"Executing pending task '{task}'");
task.Execute();
}
}
App.PendingSettingTasks.Clear();

View File

@ -5,14 +5,14 @@ using Microsoft.Win32;
using CommunityToolkit.Mvvm.Input;
using Bloxstrap.Models.SettingTasks;
namespace Bloxstrap.UI.ViewModels.Settings
{
public class ModsViewModel : NotifyPropertyChangedViewModel
{
private void OpenModsFolder() => Process.Start("explorer.exe", Paths.Modifications);
private bool _usingCustomFont => File.Exists(Paths.CustomFont);
private readonly Dictionary<string, byte[]> FontHeaders = new()
{
{ "ttf", new byte[4] { 0x00, 0x01, 0x00, 0x00 } },
@ -22,16 +22,15 @@ namespace Bloxstrap.UI.ViewModels.Settings
private void ManageCustomFont()
{
if (_usingCustomFont)
if (!String.IsNullOrEmpty(TextFontTask.NewState))
{
Filesystem.AssertReadOnly(Paths.CustomFont);
File.Delete(Paths.CustomFont);
TextFontTask.NewState = "";
}
else
{
var dialog = new OpenFileDialog
{
Filter = $"{Resources.Strings.Menu_FontFiles}|*.ttf;*.otf;*.ttc"
Filter = $"{Strings.Menu_FontFiles}|*.ttf;*.otf;*.ttc"
};
if (dialog.ShowDialog() != true)
@ -41,13 +40,11 @@ namespace Bloxstrap.UI.ViewModels.Settings
if (!FontHeaders.ContainsKey(type) || !File.ReadAllBytes(dialog.FileName).Take(4).SequenceEqual(FontHeaders[type]))
{
Frontend.ShowMessageBox(Resources.Strings.Menu_Mods_Misc_CustomFont_Invalid, MessageBoxImage.Error);
Frontend.ShowMessageBox(Strings.Menu_Mods_Misc_CustomFont_Invalid, MessageBoxImage.Error);
return;
}
Directory.CreateDirectory(Path.GetDirectoryName(Paths.CustomFont)!);
File.Copy(dialog.FileName, Paths.CustomFont);
Filesystem.AssertReadOnly(Paths.CustomFont);
TextFontTask.NewState = dialog.FileName;
}
OnPropertyChanged(nameof(ChooseCustomFontVisibility));
@ -56,45 +53,49 @@ namespace Bloxstrap.UI.ViewModels.Settings
public ICommand OpenModsFolderCommand => new RelayCommand(OpenModsFolder);
public bool OldDeathSoundEnabled
{
get => App.Settings.Prop.UseOldDeathSound;
set => App.Settings.Prop.UseOldDeathSound = value;
}
public Visibility ChooseCustomFontVisibility => !String.IsNullOrEmpty(TextFontTask.NewState) ? Visibility.Collapsed : Visibility.Visible;
public bool OldCharacterSoundsEnabled
{
get => App.Settings.Prop.UseOldCharacterSounds;
set => App.Settings.Prop.UseOldCharacterSounds = value;
}
public IReadOnlyCollection<Enums.CursorType> CursorTypes => CursorTypeEx.Selections;
public Enums.CursorType SelectedCursorType
{
get => App.Settings.Prop.CursorType;
set => App.Settings.Prop.CursorType = value;
}
public bool OldAvatarBackground
{
get => App.Settings.Prop.UseOldAvatarBackground;
set => App.Settings.Prop.UseOldAvatarBackground = value;
}
public IReadOnlyCollection<EmojiType> EmojiTypes => EmojiTypeEx.Selections;
public EmojiType SelectedEmojiType
{
get => App.Settings.Prop.EmojiType;
set => App.Settings.Prop.EmojiType = value;
}
public Visibility ChooseCustomFontVisibility => _usingCustomFont ? Visibility.Collapsed : Visibility.Visible;
public Visibility DeleteCustomFontVisibility => _usingCustomFont ? Visibility.Visible : Visibility.Collapsed;
public Visibility DeleteCustomFontVisibility => !String.IsNullOrEmpty(TextFontTask.NewState) ? Visibility.Visible : Visibility.Collapsed;
public ICommand ManageCustomFontCommand => new RelayCommand(ManageCustomFont);
public ModPresetTask OldDeathSoundTask { get; } = new("OldDeathSound", @"content\sounds\ouch.ogg", "Sounds.OldDeath.ogg");
public ModPresetTask OldAvatarBackgroundTask { get; } = new("OldAvatarBackground", @"ExtraContent\places\Mobile.rbxl", "OldAvatarBackground.rbxl");
public ModPresetTask OldCharacterSoundsTask { get; } = new("OldCharacterSounds", new()
{
{ @"content\sounds\action_footsteps_plastic.mp3", "Sounds.OldWalk.mp3" },
{ @"content\sounds\action_jump.mp3", "Sounds.OldJump.mp3" },
{ @"content\sounds\action_get_up.mp3", "Sounds.OldGetUp.mp3" },
{ @"content\sounds\action_falling.mp3", "Sounds.Empty.mp3" },
{ @"content\sounds\action_jump_land.mp3", "Sounds.Empty.mp3" },
{ @"content\sounds\action_swim.mp3", "Sounds.Empty.mp3" },
{ @"content\sounds\impact_water.mp3", "Sounds.Empty.mp3" }
});
public EmojiModPresetTask EmojiFontTask { get; } = new();
public EnumModPresetTask<Enums.CursorType> CursorTypeTask { get; } = new("CursorType", new()
{
{
Enums.CursorType.From2006, new()
{
{ @"content\textures\Cursors\KeyboardMouse\ArrowCursor.png", "Cursor.From2006.ArrowCursor.png" },
{ @"content\textures\Cursors\KeyboardMouse\ArrowFarCursor.png", "Cursor.From2006.ArrowFarCursor.png" }
}
},
{
Enums.CursorType.From2013, new()
{
{ @"content\textures\Cursors\KeyboardMouse\ArrowCursor.png", "Cursor.From2013.ArrowCursor.png" },
{ @"content\textures\Cursors\KeyboardMouse\ArrowFarCursor.png", "Cursor.From2013.ArrowFarCursor.png" }
}
}
});
public FontModPresetTask TextFontTask { get; } = new();
public bool DisableFullscreenOptimizations
{
get => App.Settings.Prop.DisableFullscreenOptimizations;

View File

@ -5,50 +5,12 @@ namespace Bloxstrap.UI.ViewModels.Settings
{
public class ShortcutsViewModel : NotifyPropertyChangedViewModel
{
private ShortcutTask _desktopIconTask = new(Path.Combine(Paths.Desktop, "Bloxstrap.lnk"))
{
Name = "DesktopIcon"
};
public ShortcutTask DesktopIconTask { get; } = new("Desktop", Paths.Desktop, "Bloxstrap.lnk");
private ShortcutTask _startMenuIconTask = new(Path.Combine(Paths.WindowsStartMenu, "Bloxstrap.lnk"))
{
Name = "StartMenuIcon"
};
public ShortcutTask StartMenuIconTask { get; } = new("StartMenu", Paths.WindowsStartMenu, "Bloxstrap.lnk");
private ShortcutTask _playerIconTask = new(Path.Combine(Paths.Desktop, $"{Strings.LaunchMenu_LaunchRoblox}.lnk"))
{
Name = "RobloxPlayerIcon",
ExeFlags = "-player"
};
public ShortcutTask PlayerIconTask { get; } = new("RobloxPlayer", Paths.Desktop, $"{Strings.LaunchMenu_LaunchRoblox}.lnk", "-player");
private ShortcutTask _settingsIconTask = new(Path.Combine(Paths.Desktop, $"{Strings.Menu_Title}.lnk"))
{
Name = "SettingsIcon",
ExeFlags = "-settings"
};
public bool DesktopIcon
{
get => _desktopIconTask.NewState;
set => _desktopIconTask.NewState = value;
}
public bool StartMenuIcon
{
get => _startMenuIconTask.NewState;
set => _startMenuIconTask.NewState = value;
}
public bool PlayerIcon
{
get => _playerIconTask.NewState;
set => _playerIconTask.NewState = value;
}
public bool SettingsIcon
{
get => _settingsIconTask.NewState;
set => _settingsIconTask.NewState = value;
}
public ShortcutTask SettingsIconTask { get; } = new("Settings", Paths.Desktop, $"{Strings.Menu_Title}.lnk", "-settings");
}
}

View File

@ -25,7 +25,7 @@ namespace Bloxstrap.Utility
return FromStream(stream);
}
private static string Stringify(byte[] hash)
public static string Stringify(byte[] hash)
{
return BitConverter.ToString(hash).Replace("-", "").ToLowerInvariant();
}