From eab44d2d94800bacf509d10c772d9d6d1d351b33 Mon Sep 17 00:00:00 2001 From: YoshiRulz Date: Thu, 30 Jan 2020 22:41:07 +1000 Subject: [PATCH] Add hack to EmuApi, add WIP external tool AutoGenConfig --- BizHawk.Client.Common/Api/Classes/EmuApi.cs | 12 ++ .../AutoGenConfig/AutoGenConfig.csproj | 7 + .../AutoGenConfig/ConfigEditorUIGenerators.cs | 145 ++++++++++++++++++ .../AutoGenConfig/CustomMainForm.cs | 131 ++++++++++++++++ .../AutoGenConfig/Properties/AssemblyInfo.cs | 41 +++++ .../AutoGenConfig/ScopingExtensions.cs | 21 +++ .../AutoGenConfig/build_debug.sh | 1 + .../AutoGenConfig/build_release.sh | 1 + 8 files changed, 359 insertions(+) create mode 100644 ExternalToolProjects/AutoGenConfig/AutoGenConfig.csproj create mode 100644 ExternalToolProjects/AutoGenConfig/ConfigEditorUIGenerators.cs create mode 100644 ExternalToolProjects/AutoGenConfig/CustomMainForm.cs create mode 100644 ExternalToolProjects/AutoGenConfig/Properties/AssemblyInfo.cs create mode 100644 ExternalToolProjects/AutoGenConfig/ScopingExtensions.cs create mode 120000 ExternalToolProjects/AutoGenConfig/build_debug.sh create mode 120000 ExternalToolProjects/AutoGenConfig/build_release.sh diff --git a/BizHawk.Client.Common/Api/Classes/EmuApi.cs b/BizHawk.Client.Common/Api/Classes/EmuApi.cs index a353ff8890..81380d4265 100644 --- a/BizHawk.Client.Common/Api/Classes/EmuApi.cs +++ b/BizHawk.Client.Common/Api/Classes/EmuApi.cs @@ -47,8 +47,20 @@ namespace BizHawk.Client.Common private readonly Action LogCallback; + /// Using this property to get a reference to Global.Config is a terrible, horrible, no good, very bad idea. That's why it's not in the interface. + public Config ForbiddenConfigReference + { + get + { + ForbiddenConfigReferenceUsed = true; + return Global.Config; + } + } + public Action FrameAdvanceCallback { get; set; } + public bool ForbiddenConfigReferenceUsed { get; private set; } + public Action YieldCallback { get; set; } public void DisplayVsync(bool enabled) => Global.Config.VSync = enabled; diff --git a/ExternalToolProjects/AutoGenConfig/AutoGenConfig.csproj b/ExternalToolProjects/AutoGenConfig/AutoGenConfig.csproj new file mode 100644 index 0000000000..b5f4c56262 --- /dev/null +++ b/ExternalToolProjects/AutoGenConfig/AutoGenConfig.csproj @@ -0,0 +1,7 @@ + + + + + + + diff --git a/ExternalToolProjects/AutoGenConfig/ConfigEditorUIGenerators.cs b/ExternalToolProjects/AutoGenConfig/ConfigEditorUIGenerators.cs new file mode 100644 index 0000000000..156298b060 --- /dev/null +++ b/ExternalToolProjects/AutoGenConfig/ConfigEditorUIGenerators.cs @@ -0,0 +1,145 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Drawing; +using System.Linq; +using System.Reflection; +using System.Windows.Forms; + +using BizHawk.Client.Common; + +namespace BizHawk.Client.EmuHawk +{ + public static class ConfigEditorUIGenerators + { + public static readonly IDictionary> FallbackGenerators = new Dictionary> { + [typeof(bool)] = new CheckBoxForBoolEditorUIGen(), + [typeof(string)] = new TextBoxForStringEditorUIGen() + }; + + public static readonly IConfigPropEditorUIGen FinalFallbackGenerator = new UnrepresentablePropEditorUIGen(); + + private static Color GetComparisonColorRefT(string prop, T? currentValue, CustomMainForm parent, Func equalityFunc) + where T : class + => equalityFunc(currentValue, parent.BaselineValues[prop] as T) + ? GetInitComparisonColorRefT(prop, currentValue, equalityFunc) + : equalityFunc(currentValue, CustomMainForm.DefaultValues[prop] as T) + ? CustomMainForm.ComparisonColors.ChangedUnset + : CustomMainForm.ComparisonColors.Changed; + + private static Color GetComparisonColorValT(string prop, T? currentValue, CustomMainForm parent, Func equalityFunc) + where T : struct + => equalityFunc(currentValue, parent.BaselineValues[prop]?.Let(it => (T) it)) + ? GetInitComparisonColorValT(prop, currentValue, equalityFunc) + : equalityFunc(currentValue, CustomMainForm.DefaultValues[prop]?.Let(it => (T) it)) + ? CustomMainForm.ComparisonColors.ChangedUnset + : CustomMainForm.ComparisonColors.Changed; + + private static Color GetInitComparisonColorRefT(string prop, T? currentValue, Func equalityFunc) + where T : class + => equalityFunc(currentValue, CustomMainForm.DefaultValues[prop] as T) + ? CustomMainForm.ComparisonColors.UnchangedDefault + : CustomMainForm.ComparisonColors.Unchanged; + + private static Color GetInitComparisonColorValT(string prop, T? currentValue, Func equalityFunc) + where T : struct + => equalityFunc(currentValue, CustomMainForm.DefaultValues[prop]?.Let(it => (T) it)) + ? CustomMainForm.ComparisonColors.UnchangedDefault + : CustomMainForm.ComparisonColors.Unchanged; + + private static CustomMainForm GetMainFormParent(Control c) + { + var parent = c.Parent; + while (!(parent is CustomMainForm)) parent = parent.Parent; + return (CustomMainForm) parent; + } + + private static string GetPropertyNameDesc(PropertyInfo pi) + => pi.GetCustomAttributes(typeof(DescriptionAttribute), false).FirstOrDefault() + ?.Let(it => $"{pi.Name}: {((DescriptionAttribute) it).Description}") + ?? pi.Name; + + public struct ComparisonColors + { + public Color Changed; + public Color ChangedInvalid; + public Color ChangedUnset; + public Color Unchanged; + public Color UnchangedDefault; + } + + public interface IConfigPropEditorUIGen + where TControl : Control + { + TControl GenerateControl(PropertyInfo pi, Config config, IDictionary baselineValues); + } + + private class CheckBoxForBoolEditorUIGen : IConfigPropEditorUIGen + { + private static bool BoolEquality(bool? a, bool? b) => a == b; + + private static void CheckBoxClickHandler(object clickEventSender, EventArgs clickEventArgs) + => ((CheckBox) clickEventSender).Let(cb => + cb.ForeColor = GetComparisonColorValT(((PropertyInfo) cb.Tag).Name, cb.Checked, GetMainFormParent(cb), BoolEquality) + ); + + public CheckBox GenerateControl(PropertyInfo pi, Config config, IDictionary baselineValues) + { + if (pi.PropertyType != typeof(bool)) throw new Exception(); + var baseline = (bool) pi.GetValue(config); + baselineValues[pi.Name] = baseline; + return new CheckBox + { + AutoSize = true, + Checked = baseline, + ForeColor = GetInitComparisonColorValT(pi.Name, baseline, BoolEquality), + Tag = pi, + Text = GetPropertyNameDesc(pi) + }.Also(it => it.Click += CheckBoxClickHandler); + } + } + + private class TextBoxForStringEditorUIGen : IConfigPropEditorUIGen + { + private static readonly Func StringEquality = string.Equals; + + public FlowLayoutPanel GenerateControl(PropertyInfo pi, Config config, IDictionary baselineValues) + { + if (pi.PropertyType != typeof(string)) throw new Exception(); + var baseline = (string) pi.GetValue(config); + baselineValues[pi.Name] = baseline; + return new FlowLayoutPanel { + AutoSize = true, + Controls = { + new Label { Anchor = AnchorStyles.None, AutoSize = true, Text = GetPropertyNameDesc(pi) }, + new TextBox { AutoSize = true, Tag = pi, Text = baseline }.Also(it => it.TextChanged += TextBoxChangedHandler) + }, + ForeColor = GetInitComparisonColorRefT(pi.Name, baseline, StringEquality) + }; + } + + private static void TextBoxChangedHandler(object changedEventSender, EventArgs changedEventArgs) + => ((TextBox) changedEventSender).Let(tb => + tb.Parent.ForeColor = GetComparisonColorRefT(((PropertyInfo) tb.Tag).Name, tb.Text, GetMainFormParent(tb), StringEquality) + ); + } + + private class UnrepresentablePropEditorUIGen : IConfigPropEditorUIGen + { + public GroupBox GenerateControl(PropertyInfo pi, Config config, IDictionary baselineValues) + => new GroupBox { + AutoSize = true, + Controls = { + new FlowLayoutPanel { + AutoSize = true, + Controls = { new Label { AutoSize = true, Text = $"no editor found for type {pi.PropertyType}" } }, + Location = new Point(4, 16), + MaximumSize = new Size(int.MaxValue, 20) + } + }, + MaximumSize = new Size(int.MaxValue, 40), + Text = pi.Name + }; + } + } +} diff --git a/ExternalToolProjects/AutoGenConfig/CustomMainForm.cs b/ExternalToolProjects/AutoGenConfig/CustomMainForm.cs new file mode 100644 index 0000000000..8e2f2b8a51 --- /dev/null +++ b/ExternalToolProjects/AutoGenConfig/CustomMainForm.cs @@ -0,0 +1,131 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Drawing; +using System.Linq; +using System.Reflection; +using System.Windows.Forms; + +using BizHawk.Client.ApiHawk; +using BizHawk.Client.Common; + +using static BizHawk.Client.EmuHawk.ConfigEditorUIGenerators; + +namespace BizHawk.Client.EmuHawk +{ + public class CustomMainForm : Form, IExternalToolForm + { + private static readonly IList<(PropertyInfo, IConfigPropEditorUIGen)> CachedControlGenerators; + + public static ComparisonColors ComparisonColors = new ComparisonColors + { + Changed = Color.FromArgb(unchecked((int) 0xFF9F3F00)), + ChangedInvalid = Color.DarkRed, + ChangedUnset = Color.FromArgb(unchecked((int) 0xFF9F1F5F)), + Unchanged = Color.FromArgb(unchecked((int) 0xFF00003F)), + UnchangedDefault = Color.Black + }; + + public static readonly IDictionary DefaultValues; + + static CustomMainForm() + { + CachedControlGenerators = new List<(PropertyInfo, IConfigPropEditorUIGen)>(); + DefaultValues = new Dictionary(); + foreach (var pi in typeof(Config).GetProperties()) + { + CachedControlGenerators.Add((pi, FallbackGenerators.TryGetValue(pi.PropertyType, out var gen) ? gen : FinalFallbackGenerator)); + DefaultValues[pi.Name] = pi.GetCustomAttributes(typeof(DefaultValueAttribute), false).FirstOrDefault() + ?.Let(it => ((DefaultValueAttribute) it).Value) + ?? TrueGenericDefault(pi.PropertyType); + } + } + + /// value types: default(T); ref types: calls default (no-arg) ctor if it exists, else null + private static object? TrueGenericDefault(Type t) + { + try + { + return Activator.CreateInstance(t); + } + catch + { + return null; + } + } + + public readonly IDictionary BaselineValues = new Dictionary(); + + [RequiredApi] + private IEmu? EmuHawkAPI { get; set; } + + public override string Text => "AutoGenConfig"; + + public bool UpdateBefore => false; + + public CustomMainForm() + { + ClientSize = new Size(640, 720); + SuspendLayout(); + Controls.Add(new FlowLayoutPanel { + Anchor = AnchorStyles.Top | AnchorStyles.Left | AnchorStyles.Right, + BorderStyle = BorderStyle.FixedSingle, + Controls = { + new Label { AutoSize = true, Text = "Legend:" }, + new Label { AutoSize = true, ForeColor = ComparisonColors.UnchangedDefault, Text = "default, unchanged" }, + new Label { AutoSize = true, ForeColor = ComparisonColors.Unchanged, Text = "custom, unchanged" }, + new Label { AutoSize = true, ForeColor = ComparisonColors.ChangedUnset, Text = "custom => default" }, + new Label { AutoSize = true, ForeColor = ComparisonColors.ChangedInvalid, Text = "invalid" }, + new Label { AutoSize = true, ForeColor = ComparisonColors.Changed, Text = "custom A => custom B" } + }, + Location = new Point(4, 4), + Padding = new Padding(0, 4, 0, 0), + Size = new Size(ClientSize.Width - 8, 24), + WrapContents = false + }); + FlowLayoutPanel flpMain; + Controls.Add(flpMain = new FlowLayoutPanel { + Anchor = AnchorStyles.Top | AnchorStyles.Bottom | AnchorStyles.Left | AnchorStyles.Right, + AutoScroll = true, + FlowDirection = FlowDirection.TopDown, + Location = new Point(4, 32), + Size = new Size(ClientSize.Width - 8, ClientSize.Height - 64), + WrapContents = false + }); + Controls.Add(new FlowLayoutPanel { + Anchor = AnchorStyles.Bottom | AnchorStyles.Right, + AutoScroll = true, + AutoSize = true, + Controls = { + new Button { + Size = new Size(128, 24), + Text = "Discard Changes" + }.Also(it => it.Click += (clickEventSender, clickEventArgs) => Close()), + new Button { + Size = new Size(128, 24), + Text = "Review and Save..." + }.Also(it => it.Click += (clickEventSender, clickEventArgs) => Close()) + }, + FlowDirection = FlowDirection.RightToLeft, + Location = new Point(ClientSize.Width - 201, ClientSize.Height - 31), + WrapContents = false + }); + Load += (loadEventSender, loadEventArgs) => + { + var config = (EmuHawkAPI as EmuApi ?? throw new Exception("required API wasn't fulfilled")).ForbiddenConfigReference; + flpMain.Controls.AddRange(CachedControlGenerators.Select(it => it.Item2.GenerateControl(it.Item1, config, BaselineValues)).ToArray()); + }; + ResumeLayout(); + } + + public bool AskSaveChanges() => true; + + public void FastUpdate() {} + + public void NewUpdate(ToolFormUpdateType type) {} + + public void Restart() {} + + public void UpdateValues() {} + } +} diff --git a/ExternalToolProjects/AutoGenConfig/Properties/AssemblyInfo.cs b/ExternalToolProjects/AutoGenConfig/Properties/AssemblyInfo.cs new file mode 100644 index 0000000000..6e1ef1482f --- /dev/null +++ b/ExternalToolProjects/AutoGenConfig/Properties/AssemblyInfo.cs @@ -0,0 +1,41 @@ +using System.Drawing; +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using BizHawk.Client.ApiHawk; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("AutoGenConfig")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("AutoGenConfig")] +[assembly: AssemblyCopyright("Copyright © 2015")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +[assembly: BizHawkExternalTool("AutoGenConfig")] +[assembly: BizHawkExternalToolUsage()] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("34ef725e-ecfd-4e25-b7fd-5c995dd62eef")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/ExternalToolProjects/AutoGenConfig/ScopingExtensions.cs b/ExternalToolProjects/AutoGenConfig/ScopingExtensions.cs new file mode 100644 index 0000000000..6c2f9bbfa1 --- /dev/null +++ b/ExternalToolProjects/AutoGenConfig/ScopingExtensions.cs @@ -0,0 +1,21 @@ +using System; +using System.Runtime.CompilerServices; + +namespace BizHawk.Client.EmuHawk +{ + internal static class ScopingExtensions + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static T Also(this T receiver, Action action) where T : notnull + { + action(receiver); + return receiver; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void Let(this T receiver, Action func) where T : notnull => func(receiver); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static TReturn Let(this TRec receiver, Func func) where TRec : notnull => func(receiver); + } +} diff --git a/ExternalToolProjects/AutoGenConfig/build_debug.sh b/ExternalToolProjects/AutoGenConfig/build_debug.sh new file mode 120000 index 0000000000..c4d41e9cbf --- /dev/null +++ b/ExternalToolProjects/AutoGenConfig/build_debug.sh @@ -0,0 +1 @@ +../.build_net48_from_cwd_debug.sh \ No newline at end of file diff --git a/ExternalToolProjects/AutoGenConfig/build_release.sh b/ExternalToolProjects/AutoGenConfig/build_release.sh new file mode 120000 index 0000000000..73e473fcc2 --- /dev/null +++ b/ExternalToolProjects/AutoGenConfig/build_release.sh @@ -0,0 +1 @@ +../.build_net48_from_cwd_release.sh \ No newline at end of file