BUTR/Bannerlord.BLSE

View on GitHub
src/Bannerlord.LauncherEx/ViewModels/BUTRLauncherOptionsVM.cs

Summary

Maintainability
F
4 days
Test Coverage
using Bannerlord.BUTR.Shared.Extensions;
using Bannerlord.LauncherEx.Helpers;
using Bannerlord.LauncherEx.Options;
using Bannerlord.LauncherManager.Localization;

using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.IO;
using System.Text;

using TaleWorlds.Library;

namespace Bannerlord.LauncherEx.ViewModels;

internal enum OptionsType
{
    Launcher, Game, Engine
}

internal sealed class BUTRLauncherOptionsVM : BUTRViewModel
{
    private readonly BUTRLauncherManagerHandler _launcherManagerHandler = BUTRLauncherManagerHandler.Default;
    private readonly OptionsType _optionsType;
    private LauncherExData? _launcherExData;
    private readonly Action _saveUserData;
    private readonly Action _refreshOptions;

    [BUTRDataSourceProperty]
    public bool IsDisabled { get => _isDisabled; set => SetField(ref _isDisabled, value); }
    private bool _isDisabled;

    [BUTRDataSourceProperty]
    public MBBindingList<SettingsPropertyVM> SettingProperties { get => _settingProperties; set => SetField(ref _settingProperties, value); }
    private MBBindingList<SettingsPropertyVM> _settingProperties = new();

    [BUTRDataSourceProperty]
    public string NeedsGameLaunchMessage { get => _needsGameLaunchMessage; set => SetField(ref _needsGameLaunchMessage, value); }
    private string _needsGameLaunchMessage = new BUTRTextObject("{=jfNh7Sg3}One-time game launch is required!").ToString();

    [BUTRDataSourceProperty]
    public bool NeedsGameLaunch { get => _needsGameLaunch; set => SetField(ref _needsGameLaunch, value); }
    private bool _needsGameLaunch;

    public BUTRLauncherOptionsVM(OptionsType optionsType, Action saveUserData, Action refreshOptions)
    {
        _optionsType = optionsType;
        _saveUserData = saveUserData;
        _refreshOptions = refreshOptions;
    }

    public void Refresh()
    {
        SettingProperties.Clear();
        switch (_optionsType)
        {
            case OptionsType.Launcher:
                RefreshLauncherOptions();
                break;
            case OptionsType.Game:
                RefreshGameOptions();
                break;
            case OptionsType.Engine:
                RefreshEngineOptions();
                break;
        }
    }
    private void RefreshLauncherOptions()
    {
        _launcherExData = new LauncherExData(
            LauncherSettings.AutomaticallyCheckForUpdates,
            LauncherSettings.FixCommonIssues,
            LauncherSettings.CompactModuleList,
            LauncherSettings.HideRandomImage,
            LauncherSettings.DisableBinaryCheck,
            LauncherSettings.BetaSorting,
            LauncherSettings.BigMode,
            LauncherSettings.EnableDPIScaling,
            LauncherSettings.DisableCrashHandlerWhenDebuggerIsAttached,
            LauncherSettings.DisableCatchAutoGenExceptions,
            LauncherSettings.UseVanillaCrashHandler);

        SettingProperties.Add(new SettingsPropertyVM(new SettingsPropertyDefinition
        {
            DisplayName = new BUTRTextObject("{=LXlsSS8t}Fix Common Issues").ToString(),
            HintText = new BUTRTextObject("{=J9VbkLW4}Fixes issues like 0Harmony.dll being in the /bin folder").ToString(),
            SettingType = SettingType.Bool,
            PropertyReference = new PropertyRef(typeof(LauncherSettings).GetProperty(nameof(LauncherSettings.FixCommonIssues))!, this)
        }));
        SettingProperties.Add(new SettingsPropertyVM(new SettingsPropertyDefinition
        {
            DisplayName = new BUTRTextObject("{=vUAqDj9H}Compact Module List").ToString(),
            HintText = $"{new BUTRTextObject("{=44qrhQ6g}Requires restart!")} {new BUTRTextObject("{=Qn1aPNQM}Makes the Mods tab content smaller")}",
            SettingType = SettingType.Bool,
            PropertyReference = new PropertyRef(typeof(LauncherSettings).GetProperty(nameof(LauncherSettings.CompactModuleList))!, this)
        }));
        SettingProperties.Add(new SettingsPropertyVM(new SettingsPropertyDefinition
        {
            DisplayName = new BUTRTextObject("{=GUWbD65T}Disable Binary Compatibility Check").ToString(),
            HintText = $"{new BUTRTextObject("{=z9WqFewN}DISABLED!")} {new BUTRTextObject("{=44qrhQ6g}Requires restart!")} {new BUTRTextObject("{=lmpQeQBS}Disables Launcher's own check for binary compatibility of mods")}",
            SettingType = SettingType.Bool,
            PropertyReference = new PropertyRef(typeof(LauncherSettings).GetProperty(nameof(LauncherSettings.DisableBinaryCheck))!, this)
        }));
        SettingProperties.Add(new SettingsPropertyVM(new SettingsPropertyDefinition
        {
            DisplayName = new BUTRTextObject("{=iD27wEq7}Hide Random Image").ToString(),
            HintText = new BUTRTextObject("{=LaPvZjwC}Hide's the Rider image so the launcher looks more compact").ToString(),
            SettingType = SettingType.Bool,
            PropertyReference = new PropertyRef(typeof(LauncherSettings).GetProperty(nameof(LauncherSettings.HideRandomImage))!, this)
        }));
        SettingProperties.Add(new SettingsPropertyVM(new SettingsPropertyDefinition
        {
            DisplayName = new BUTRTextObject("{=QJSBiZdJ}Beta Sorting").ToString(),
            HintText = new BUTRTextObject("{=HVhaqeb4}Uses the new sorting algorithm after v1.12.x. Disable to use the old algorithm").ToString(),
            SettingType = SettingType.Bool,
            PropertyReference = new PropertyRef(typeof(LauncherSettings).GetProperty(nameof(LauncherSettings.BetaSorting))!, this)
        }));
        SettingProperties.Add(new SettingsPropertyVM(new SettingsPropertyDefinition
        {
            DisplayName = new BUTRTextObject("{=1zt99vTt}Big Mode").ToString(),
            HintText = new BUTRTextObject("{=XUSDSpvf}Makes the launcher bigger in height").ToString(),
            SettingType = SettingType.Bool,
            PropertyReference = new PropertyRef(typeof(LauncherSettings).GetProperty(nameof(LauncherSettings.BigMode))!, this)
        }));
        SettingProperties.Add(new SettingsPropertyVM(new SettingsPropertyDefinition
        {
            DisplayName = new BUTRTextObject("{=1zt99vTt}Enable DPI Scaling").ToString(),
            HintText = $"{new BUTRTextObject("{=44qrhQ6g}Requires restart!")} {new BUTRTextObject("{=JusnHy6S}Enables Windows DPI Scaling to remove blurriness of UI elements")}",
            SettingType = SettingType.Bool,
            PropertyReference = new PropertyRef(typeof(LauncherSettings).GetProperty(nameof(LauncherSettings.EnableDPIScaling))!, this)
        }));
        SettingProperties.Add(new SettingsPropertyVM(new SettingsPropertyDefinition
        {
            DisplayName = new BUTRTextObject("{=IsR2rbnG}Restore Game Options Backup").ToString(),
            HintText = new BUTRTextObject("{=uKUsA3Sp}LauncherEx always makes a backup before saving the first time. This will restore the original files").ToString(),
            SettingType = SettingType.Button,
            PropertyReference = new ProxyRef<string>(() => new BUTRTextObject("{=TLDgPay9}Restore").ToString(), _ =>
            {
                var backupPath = $"{ConfigReader.GameConfigPath}.bak";
                if (File.Exists(backupPath))
                {
                    File.Copy(backupPath, ConfigReader.GameConfigPath, true);
                    File.Delete(backupPath);
                    _refreshOptions();
                }
            })
        }));
        SettingProperties.Add(new SettingsPropertyVM(new SettingsPropertyDefinition
        {
            DisplayName = new BUTRTextObject("{=5XzSM7RN}Restore Engine Options Backup").ToString(),
            HintText = new BUTRTextObject("{=uKUsA3Sp}LauncherEx always makes a backup before saving the first time. This will restore the original files").ToString(),
            SettingType = SettingType.Button,
            PropertyReference = new ProxyRef<string>(() => new BUTRTextObject("{=TLDgPay9}Restore").ToString(), _ =>
            {
                var backupPath = $"{ConfigReader.EngineConfigPath}.bak";
                if (File.Exists(backupPath))
                {
                    File.Copy(backupPath, ConfigReader.EngineConfigPath, true);
                    File.Delete(backupPath);
                    _refreshOptions();
                }
            })
        }));
        /*
        SettingProperties.Add(new SettingsPropertyVM(new SettingsPropertyDefinition
        {
            DisplayName = "Automatically Check for Updates",
            SettingType = SettingType.Bool,
            PropertyReference = new PropertyRef(typeof(BUTRLoaderAppDomainManager).GetProperty(nameof(BUTRLoaderAppDomainManager.AutomaticallyCheckForUpdates))!, this)
        }));
        */
        SettingProperties.Add(new SettingsPropertyVM(new SettingsPropertyDefinition
        {
            DisplayName = new BUTRTextObject("{=QzPFvxGy}Disable BLSE Crash Handler When Debugger Is Attached").ToString(),
            HintText = new BUTRTextObject("{=P5NWQtKr}Stops BLSE Crash Handler when a debugger is attached. Do not disable if not sure.").ToString(),
            SettingType = SettingType.Bool,
            PropertyReference = new PropertyRef(typeof(LauncherSettings).GetProperty(nameof(LauncherSettings.DisableCrashHandlerWhenDebuggerIsAttached))!, this)
        }));
        SettingProperties.Add(new SettingsPropertyVM(new SettingsPropertyDefinition
        {
            DisplayName = new BUTRTextObject("{=NkCBdPSE}Disable Auto Generated Method Exception Catching").ToString(),
            HintText = new BUTRTextObject("{=QWGZy8Ym}Disables catching every Native->Managed call. It should catch every exception not catched the standard way. Do not disable if not sure.").ToString(),
            SettingType = SettingType.Bool,
            PropertyReference = new PropertyRef(typeof(LauncherSettings).GetProperty(nameof(LauncherSettings.DisableCatchAutoGenExceptions))!, this)
        }));
        SettingProperties.Add(new SettingsPropertyVM(new SettingsPropertyDefinition
        {
            DisplayName = new BUTRTextObject("{=qKK4Ehyd}Use Vanilla Crash Handler").ToString(),
            HintText = new BUTRTextObject("{=RTmWsIEA}Disables ButterLib's and BEW's Crash Handlers with the new Watchdog Crash Handler. Do not enable if not sure.").ToString(),
            SettingType = SettingType.Bool,
            PropertyReference = new PropertyRef(typeof(LauncherSettings).GetProperty(nameof(LauncherSettings.UseVanillaCrashHandler))!, this)
        }));
    }
    private void RefreshGameOptions()
    {
        try
        {
            var options = ConfigReader.GetGameOptions(path => File.Exists(path) ? File.ReadAllBytes(path) : null);
            if (options.Count == 0)
                NeedsGameLaunch = true;

            foreach (var (key, value) in options)
            {
                SettingProperties.Add(CreateSettingsPropertyVM(key, value, ToSeparateWords));
            }
        }
        catch (Exception) { /* ignore */ }
    }
    private void RefreshEngineOptions()
    {
        try
        {
            var options = ConfigReader.GetEngineOptions(path => File.Exists(path) ? File.ReadAllBytes(path) : null);
            if (options.Count == 0)
                NeedsGameLaunch = true;

            foreach (var (key, value) in options)
            {
                SettingProperties.Add(CreateSettingsPropertyVM(key, value, x => ToTitleCase(x.Replace("_", " "))));
            }
        }
        catch (Exception) { /* ignore */ }
    }

    public void Save()
    {
        switch (_optionsType)
        {
            case OptionsType.Launcher:
                SaveLauncherOptions();
                break;
            case OptionsType.Game:
                SaveGameOptions();
                break;
            case OptionsType.Engine:
                SaveEngineOptions();
                break;
        }
    }
    private void SaveLauncherOptions()
    {
        if (_launcherExData is null)
            return;

        if (_launcherExData.AutomaticallyCheckForUpdates != LauncherSettings.AutomaticallyCheckForUpdates)
        {
            _saveUserData();
            return;
        }

        if (_launcherExData.FixCommonIssues != LauncherSettings.FixCommonIssues)
        {
            _saveUserData();
            return;
        }

        if (_launcherExData.CompactModuleList != LauncherSettings.CompactModuleList)
        {
            _saveUserData();
            return;
        }

        if (_launcherExData.HideRandomImage != LauncherSettings.HideRandomImage)
        {
            _saveUserData();
            return;
        }

        if (_launcherExData.DisableBinaryCheck != LauncherSettings.DisableBinaryCheck)
        {
            _saveUserData();
            return;
        }

        if (_launcherExData.BetaSorting != LauncherSettings.BetaSorting)
        {
            _saveUserData();
            return;
        }

        if (_launcherExData.BigMode != LauncherSettings.BigMode)
        {
            _saveUserData();
            return;
        }

        if (_launcherExData.EnableDPIScaling != LauncherSettings.EnableDPIScaling)
        {
            _saveUserData();
            return;
        }

        if (_launcherExData.DisableCrashHandlerWhenDebuggerIsAttached != LauncherSettings.DisableCrashHandlerWhenDebuggerIsAttached)
        {
            _saveUserData();
            return;
        }

        if (_launcherExData.DisableCatchAutoGenExceptions != LauncherSettings.DisableCatchAutoGenExceptions)
        {
            _saveUserData();
            return;
        }

        if (_launcherExData.UseVanillaCrashHandler != LauncherSettings.UseVanillaCrashHandler)
        {
            _saveUserData();
            return;
        }
    }
    private void SaveGameOptions()
    {
        var backupPath = $"{ConfigReader.GameConfigPath}.bak";
        if (File.Exists(ConfigReader.GameConfigPath) && !File.Exists(backupPath))
            File.Copy(ConfigReader.GameConfigPath, backupPath);

        var hasChanges = false;
        var sb = new StringBuilder();
        foreach (var settingProperty in SettingProperties)
        {
            if (settingProperty.SettingPropertyDefinition is not ConfigSettingsPropertyDefinition propertyDefinition)
                continue;

            if (!string.Equals(propertyDefinition.OriginalValue, settingProperty.ValueAsString, StringComparison.Ordinal))
                hasChanges = true;

            sb.AppendLine($"{propertyDefinition.ConfigKey}={settingProperty.ValueAsString}");
        }
        if (hasChanges)
        {
            File.WriteAllText(ConfigReader.GameConfigPath, sb.ToString());
            var options = _launcherManagerHandler.GetTWOptions();
            BUTRLocalizationManager.ActiveLanguage = options.Language;
        }
    }
    private void SaveEngineOptions()
    {
        var backupPath = $"{ConfigReader.EngineConfigPath}.bak";
        if (File.Exists(ConfigReader.EngineConfigPath) && !File.Exists(backupPath))
            File.Copy(ConfigReader.EngineConfigPath, backupPath);

        var hasChanges = false;
        var sb = new StringBuilder();
        foreach (var settingProperty in SettingProperties)
        {
            if (settingProperty.SettingPropertyDefinition is not ConfigSettingsPropertyDefinition propertyDefinition)
                continue;

            if (!string.Equals(propertyDefinition.OriginalValue, settingProperty.ValueAsString, StringComparison.Ordinal))
                hasChanges = true;

            sb.AppendLine($"{propertyDefinition.ConfigKey} = {settingProperty.ValueAsString}");
        }
        if (hasChanges)
            File.WriteAllText(ConfigReader.EngineConfigPath, sb.ToString());
    }

    private static SettingsPropertyVM CreateSettingsPropertyVM(string key, string value, Func<string, string> keyProcessor)
    {
        var settingsType = bool.TryParse(value, out _) ? SettingType.Bool
            : int.TryParse(value, out _) ? SettingType.Int
            : float.TryParse(value, out _) ? SettingType.Float
            : SettingType.String;
        var storage = settingsType switch
        {
            SettingType.Bool => (IRef) new StorageRef<bool>(bool.Parse(value)),
            SettingType.Int => (IRef) new StorageRef<int>(int.Parse(value)),
            SettingType.Float => (IRef) new StorageRef<float>(float.Parse(value)),
            SettingType.String => (IRef) new StorageRef<string>(value),
            _ => throw new ArgumentOutOfRangeException()
        };
        var propertyRef = settingsType switch
        {
            SettingType.Bool => (IRef) new ProxyRef<bool>(() => (bool) storage.Value!, val => { storage.Value = val; }),
            SettingType.Int => (IRef) new ProxyRef<int>(() => (int) storage.Value!, val => { storage.Value = val; }),
            SettingType.Float => (IRef) new ProxyRef<float>(() => (float) storage.Value!, val => { storage.Value = val; }),
            SettingType.String => (IRef) new ProxyRef<string>(() => (string) storage.Value!, val => { storage.Value = val; }),
            _ => throw new ArgumentOutOfRangeException()
        };
        return new SettingsPropertyVM(new ConfigSettingsPropertyDefinition
        {
            ConfigKey = key,
            OriginalValue = value,
            DisplayName = keyProcessor(key),
            SettingType = settingsType,
            PropertyReference = propertyRef
        });
    }

    [return: NotNullIfNotNull("value")]
    private static string? ToSeparateWords(string? value)
    {
        if (value == null) return null;
        if (value.Length <= 1) return value;

        var inChars = value.ToCharArray();
        var uCWithAnyLC = new List<int>();
        var i = 0;
        while (i < inChars.Length && char.IsUpper(inChars[i])) { ++i; }

        for (; i < inChars.Length; i++)
        {
            if (!char.IsUpper(inChars[i])) continue;
            uCWithAnyLC.Add(i);
            if (++i >= inChars.Length || !char.IsUpper(inChars[i])) continue;
            while (++i < inChars.Length)
            {
                if (char.IsUpper(inChars[i])) continue;
                uCWithAnyLC.Add(i - 1);
                break;
            }
        }

        var outChars = new char[inChars.Length + uCWithAnyLC.Count];
        var lastIndex = 0;
        for (i = 0; i < uCWithAnyLC.Count; i++)
        {
            var currentIndex = uCWithAnyLC[i];
            Array.Copy(inChars, lastIndex, outChars, lastIndex + i, currentIndex - lastIndex);
            outChars[currentIndex + i] = ' ';
            lastIndex = currentIndex;
        }

        var lastPos = lastIndex + uCWithAnyLC.Count;
        Array.Copy(inChars, lastIndex, outChars, lastPos, outChars.Length - lastPos);
        return new string(outChars);
    }

    [return: NotNullIfNotNull("value")]
    private static string? ToTitleCase(string? value) => value is null ? null : CultureInfo.InvariantCulture.TextInfo.ToTitleCase(value);
}