From 5203a2a75a7d9a788fd5e7bae3295d1e09ecf82e Mon Sep 17 00:00:00 2001 From: Egor Aralov Date: Sun, 9 Jul 2017 20:38:01 +0200 Subject: [PATCH] Add to source control --- Attibutes/AbstractOptionsAttribute.cs | 60 ++++ Attibutes/ButtonAttribute.cs | 14 + Attibutes/CheckboxAttribute.cs | 14 + .../DontTranslateDescriptionAttribute.cs | 15 + Attibutes/DropDownAttribute.cs | 32 +++ Attibutes/HideConditionAttribute.cs | 10 + Attibutes/LabelAttribute.cs | 13 + Attibutes/OptionsAttribute.cs | 20 ++ Attibutes/SliderAttribute.cs | 21 ++ Attibutes/TextFieldAttribute.cs | 13 + Extensions/CommonExtensions.cs | 31 ++ Extensions/UIHelperBaseExtensions.cs | 271 ++++++++++++++++++ OptionsFramework.csproj | 36 ++- OptionsWrapper.cs | 145 ++++++++++ Util.cs | 28 ++ 15 files changed, 716 insertions(+), 7 deletions(-) create mode 100644 Attibutes/AbstractOptionsAttribute.cs create mode 100644 Attibutes/ButtonAttribute.cs create mode 100644 Attibutes/CheckboxAttribute.cs create mode 100644 Attibutes/DontTranslateDescriptionAttribute.cs create mode 100644 Attibutes/DropDownAttribute.cs create mode 100644 Attibutes/HideConditionAttribute.cs create mode 100644 Attibutes/LabelAttribute.cs create mode 100644 Attibutes/OptionsAttribute.cs create mode 100644 Attibutes/SliderAttribute.cs create mode 100644 Attibutes/TextFieldAttribute.cs create mode 100644 Extensions/CommonExtensions.cs create mode 100644 Extensions/UIHelperBaseExtensions.cs create mode 100644 OptionsWrapper.cs create mode 100644 Util.cs diff --git a/Attibutes/AbstractOptionsAttribute.cs b/Attibutes/AbstractOptionsAttribute.cs new file mode 100644 index 0000000..907bada --- /dev/null +++ b/Attibutes/AbstractOptionsAttribute.cs @@ -0,0 +1,60 @@ +using System; +using System.Reflection; + +namespace OptionsFramework.Attibutes +{ + [AttributeUsage(AttributeTargets.Property)] + public abstract class AbstractOptionsAttribute : Attribute + { + protected AbstractOptionsAttribute(string description, string group, string actionClass, string actionMethod) + { + Description = description; + Group = group; + ActionClass = actionClass; + ActionMethod = actionMethod; + } + + public string Description { get; } + public string Group { get; } + + public Action Action() + { + if (ActionClass == null || ActionMethod == null) + { + return s => { }; + } + var method = Util.FindType(ActionClass).GetMethod(ActionMethod, BindingFlags.Public | BindingFlags.Static); + if (method == null) + { + return s => { }; + } + return s => + { + method.Invoke(null, new object[] { s }); + }; + } + + public Action Action() + { + if (ActionClass == null || ActionMethod == null) + { + return () => { }; + } + var method = Util.FindType(ActionClass).GetMethod(ActionMethod, BindingFlags.Public | BindingFlags.Static); + if (method == null) + { + return () => { }; + } + return () => + { + method.Invoke(null, new object[] { }); + }; + } + + private string ActionClass { get; } + + private string ActionMethod { get; } + + + } +} \ No newline at end of file diff --git a/Attibutes/ButtonAttribute.cs b/Attibutes/ButtonAttribute.cs new file mode 100644 index 0000000..ff1db8f --- /dev/null +++ b/Attibutes/ButtonAttribute.cs @@ -0,0 +1,14 @@ +using System; + +namespace OptionsFramework.Attibutes +{ + [AttributeUsage(AttributeTargets.Property)] + public class ButtonAttribute : AbstractOptionsAttribute + { + public ButtonAttribute(string description, string group, string actionClass = null, string actionMethod = null) : + base(description, group, actionClass, actionMethod) + { + + } + } +} \ No newline at end of file diff --git a/Attibutes/CheckboxAttribute.cs b/Attibutes/CheckboxAttribute.cs new file mode 100644 index 0000000..7dff9ec --- /dev/null +++ b/Attibutes/CheckboxAttribute.cs @@ -0,0 +1,14 @@ +using System; + +namespace OptionsFramework.Attibutes +{ + [AttributeUsage(AttributeTargets.Property)] + public class CheckboxAttribute : AbstractOptionsAttribute + { + + public CheckboxAttribute(string description, string group = null, string actionClass = null, string actionMethod = null) : + base(description, group, actionClass, actionMethod) + { + } + } +} \ No newline at end of file diff --git a/Attibutes/DontTranslateDescriptionAttribute.cs b/Attibutes/DontTranslateDescriptionAttribute.cs new file mode 100644 index 0000000..d24c772 --- /dev/null +++ b/Attibutes/DontTranslateDescriptionAttribute.cs @@ -0,0 +1,15 @@ +using System; +using System.ComponentModel; + +namespace OptionsFramework.Attibutes +{ + [AttributeUsage(AttributeTargets.All)] + public class DontTranslateDescriptionAttribute : DescriptionAttribute + { + public DontTranslateDescriptionAttribute(string description) : + base(description) + { + + } + } +} \ No newline at end of file diff --git a/Attibutes/DropDownAttribute.cs b/Attibutes/DropDownAttribute.cs new file mode 100644 index 0000000..8560289 --- /dev/null +++ b/Attibutes/DropDownAttribute.cs @@ -0,0 +1,32 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Linq; + +namespace OptionsFramework.Attibutes +{ + [AttributeUsage(AttributeTargets.Property)] + public class DropDownAttribute : AbstractOptionsAttribute + { + public DropDownAttribute(string description, string itemsClass, string group = null, string actionClass = null, + string actionMethod = null) : base(description, group, actionClass, actionMethod) + { + ItemsClass = itemsClass; + } + + public IList> GetItems(Func translator = null) + { + var type = Util.FindType(ItemsClass); + var enumValues = Enum.GetValues(type); + return (from object enumValue in enumValues + let code = (int) enumValue + let memInfo = type.GetMember(Enum.GetName(type, enumValue)) + let attributes = memInfo[0].GetCustomAttributes(typeof(DescriptionAttribute), false) + let description = ((DescriptionAttribute) attributes[0]).Description + let translatedDesctiption = translator == null ? description : translator.Invoke(description) + select new KeyValuePair(translatedDesctiption, code)).ToList(); + } + + private string ItemsClass { get; } + } +} \ No newline at end of file diff --git a/Attibutes/HideConditionAttribute.cs b/Attibutes/HideConditionAttribute.cs new file mode 100644 index 0000000..426c555 --- /dev/null +++ b/Attibutes/HideConditionAttribute.cs @@ -0,0 +1,10 @@ +using System; + +namespace OptionsFramework.Attibutes +{ + [AttributeUsage(AttributeTargets.Property)] + public abstract class HideConditionAttribute : Attribute + { + public abstract bool IsHidden(); + } +} \ No newline at end of file diff --git a/Attibutes/LabelAttribute.cs b/Attibutes/LabelAttribute.cs new file mode 100644 index 0000000..ca54edc --- /dev/null +++ b/Attibutes/LabelAttribute.cs @@ -0,0 +1,13 @@ +using System; + +namespace OptionsFramework.Attibutes +{ + [AttributeUsage(AttributeTargets.Property)] + public class LabelAttribute : AbstractOptionsAttribute + { + public LabelAttribute(string description, string group) : + base(description, group, null, null) + { + } + } +} \ No newline at end of file diff --git a/Attibutes/OptionsAttribute.cs b/Attibutes/OptionsAttribute.cs new file mode 100644 index 0000000..40b9921 --- /dev/null +++ b/Attibutes/OptionsAttribute.cs @@ -0,0 +1,20 @@ +using System; + +namespace OptionsFramework.Attibutes +{ + [AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct)] + public class OptionsAttribute : Attribute + { + public OptionsAttribute(string fileName, string legacyFileName = "") + { + FileName = fileName; + LegacyFileName = legacyFileName; + } + + //file name in local app data + public string FileName { get; } + + //file name in Cities: Skylines folder + public string LegacyFileName { get; } + } +} \ No newline at end of file diff --git a/Attibutes/SliderAttribute.cs b/Attibutes/SliderAttribute.cs new file mode 100644 index 0000000..a92caf7 --- /dev/null +++ b/Attibutes/SliderAttribute.cs @@ -0,0 +1,21 @@ +using System; + +namespace OptionsFramework.Attibutes +{ + [AttributeUsage(AttributeTargets.Property)] + public class SliderAttribute : AbstractOptionsAttribute + { + public SliderAttribute(string description, float min, float max, float step, string group = null, string actionClass = null, string actionMethod = null) : base(description, group, actionClass, actionMethod) + { + Min = min; + Max = max; + Step = step; + } + + public float Min { get; private set; } + + public float Max { get; private set; } + + public float Step { get; private set; } + } +} diff --git a/Attibutes/TextFieldAttribute.cs b/Attibutes/TextFieldAttribute.cs new file mode 100644 index 0000000..ddfe944 --- /dev/null +++ b/Attibutes/TextFieldAttribute.cs @@ -0,0 +1,13 @@ +using System; + +namespace OptionsFramework.Attibutes +{ + [AttributeUsage(AttributeTargets.Property)] + public class TextfieldAttribute : AbstractOptionsAttribute + { + public TextfieldAttribute(string description, string group = null, string actionClass = null, + string actionMethod = null) : base(description, group, actionClass, actionMethod) + { + } + } +} \ No newline at end of file diff --git a/Extensions/CommonExtensions.cs b/Extensions/CommonExtensions.cs new file mode 100644 index 0000000..fd4d987 --- /dev/null +++ b/Extensions/CommonExtensions.cs @@ -0,0 +1,31 @@ +using System; +using OptionsFramework.Attibutes; + +namespace OptionsFramework.Extensions +{ + public static class CommonExtensions + { + public static string GetPropertyDescription(this T value, string propertyName) + { + var fi = value.GetType().GetProperty(propertyName); + var attributes = + (AbstractOptionsAttribute[]) fi.GetCustomAttributes(typeof(AbstractOptionsAttribute), false); + return attributes.Length > 0 ? attributes[0].Description : throw new Exception($"Property {propertyName} wasn't annotated with AbstractOptionsAttribute"); + } + + public static string GetPropertyGroup(this T value, string propertyName) + { + var fi = value.GetType().GetProperty(propertyName); + var attributes = + (AbstractOptionsAttribute[]) fi.GetCustomAttributes(typeof(AbstractOptionsAttribute), false); + return attributes.Length > 0 ? attributes[0].Group : throw new Exception($"Property {propertyName} wasn't annotated with AbstractOptionsAttribute"); + } + + public static TR GetAttribute(this T value, string propertyName)where TR : Attribute + { + var fi = value.GetType().GetProperty(propertyName); + var attributes = (TR[])fi.GetCustomAttributes(typeof(TR), false); + return attributes.Length != 1 ? null : attributes[0]; + } + } +} \ No newline at end of file diff --git a/Extensions/UIHelperBaseExtensions.cs b/Extensions/UIHelperBaseExtensions.cs new file mode 100644 index 0000000..725eb14 --- /dev/null +++ b/Extensions/UIHelperBaseExtensions.cs @@ -0,0 +1,271 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Globalization; +using System.Linq; +using System.Reflection; +using ColossalFramework.Plugins; +using ColossalFramework.UI; +using OptionsFramework.Attibutes; +using ICities; +using UnityEngine; + +namespace OptionsFramework.Extensions +{ + public static class UIHelperBaseExtensions + { + public static IEnumerable AddOptionsGroup(this UIHelperBase helper, Func translator = null) + { + var result = new List(); + var properties = from property in typeof(T).GetProperties().Where(p => + { + var attributes = + (AbstractOptionsAttribute[])p.GetCustomAttributes(typeof(AbstractOptionsAttribute), false); + return attributes.Any(); + }).Where(p => + { + var attributes = + (HideConditionAttribute[])p.GetCustomAttributes(typeof(HideConditionAttribute), false); + return !attributes.Any(a => a.IsHidden()); + }) select property.Name; + var groups = new Dictionary(); + foreach (var propertyName in properties.ToArray()) + { + var description = OptionsWrapper.Options.GetPropertyDescription(propertyName); + var groupName = OptionsWrapper.Options.GetPropertyGroup(propertyName); + if (groupName == null) + { + var component = helper.ProcessProperty(propertyName, description, translator); + if (component != null) + { + result.Add(component); + } + } + else + { + if (translator != null) + { + groupName = translator.Invoke(groupName); + } + if (!groups.ContainsKey(groupName)) + { + groups[groupName] = helper.AddGroup(groupName); + } + var component = groups[groupName].ProcessProperty(propertyName, description, translator); + if (component != null) + { + result.Add(component); + } + } + } + return result; + } + + private static UIComponent ProcessProperty(this UIHelperBase group, string propertyName, string description, Func translator = null) + { + if (translator != null) + { + description = translator.Invoke(description); + } + UIComponent component = null; + var checkboxAttribute = OptionsWrapper.Options.GetAttribute(propertyName); + if (checkboxAttribute != null) + { + component = group.AddCheckbox(description, propertyName, checkboxAttribute); + } + var textfieldAttribute = OptionsWrapper.Options.GetAttribute(propertyName); + if (textfieldAttribute != null) + { + component = group.AddTextfield(description, propertyName, textfieldAttribute); + } + var dropDownAttribute = OptionsWrapper.Options.GetAttribute(propertyName); + if (dropDownAttribute != null) + { + component = group.AddDropdown(description, propertyName, dropDownAttribute, translator); + } + var sliderAttribute = OptionsWrapper.Options.GetAttribute(propertyName); + if (sliderAttribute != null) + { + component = group.AddSlider(description, propertyName, sliderAttribute); + } + var buttonAttribute = OptionsWrapper.Options.GetAttribute(propertyName); + if (buttonAttribute != null) + { + component = group.AddButton(description, buttonAttribute); + } + var labelAttribute = OptionsWrapper.Options.GetAttribute(propertyName); + if (labelAttribute != null) + { + component = group.AddLabel(description); + } + //TODO: more control types + + var descriptionAttribute = OptionsWrapper.Options.GetAttribute(propertyName); + if (component != null && descriptionAttribute != null) + { + component.tooltip = (translator == null || descriptionAttribute is DontTranslateDescriptionAttribute) ? descriptionAttribute.Description : translator.Invoke(descriptionAttribute.Description); + } + return component; + } + + private static UIDropDown AddDropdown(this UIHelperBase group, string text, string propertyName, DropDownAttribute attr, Func translator = null) + { + var property = typeof(T).GetProperty(propertyName); + var defaultCode = (int)property.GetValue(OptionsWrapper.Options, null); + int defaultSelection; + var items = attr.GetItems(translator); + try + { + defaultSelection = items.First(kvp => kvp.Value == defaultCode).Value; + } + catch + { + defaultSelection = 0; + property.SetValue(OptionsWrapper.Options, items.First().Value, null); + } + return (UIDropDown)group.AddDropdown(text, items.Select(kvp => kvp.Key).ToArray(), defaultSelection, sel => + { + var code = items[sel].Value; + property.SetValue(OptionsWrapper.Options, code, null); + OptionsWrapper.SaveOptions(); + attr.Action().Invoke(code); + }); + } + + private static UICheckBox AddCheckbox(this UIHelperBase group, string text, string propertyName, CheckboxAttribute attr) + { + var property = typeof(T).GetProperty(propertyName); + return (UICheckBox)group.AddCheckbox(text, (bool)property.GetValue(OptionsWrapper.Options, null), + b => + { + property.SetValue(OptionsWrapper.Options, b, null); + OptionsWrapper.SaveOptions(); + attr.Action().Invoke(b); + }); + } + + private static UIButton AddButton(this UIHelperBase group, string text, ButtonAttribute attr) + { + return (UIButton)group.AddButton(text, ()=> + { + attr.Action().Invoke(); + }); + } + + private static UILabel AddLabel(this UIHelperBase group, string text) + { + var space = (UIPanel)group.AddSpace(20); + var valueLabel = space.AddUIComponent(); + valueLabel.AlignTo(space, UIAlignAnchor.TopLeft); + valueLabel.relativePosition = new Vector3(0, 0, 0); + valueLabel.text = text; + valueLabel.Show(); + return valueLabel; + } + + private static UITextField AddTextfield(this UIHelperBase group, string text, string propertyName, TextfieldAttribute attr) + { + var property = typeof(T).GetProperty(propertyName); + var initialValue = Convert.ToString(property.GetValue(OptionsWrapper.Options, null)); + return (UITextField)group.AddTextfield(text, initialValue, s => { }, + s => + { + object value; + if (property.PropertyType == typeof(int)) + { + value = Convert.ToInt32(s); + } + else if (property.PropertyType == typeof(short)) + { + value = Convert.ToInt16(s); + } + else if (property.PropertyType == typeof(double)) + { + value = Convert.ToDouble(s); + } + else if (property.PropertyType == typeof(float)) + { + value = Convert.ToSingle(s); + } + else + { + value = s; //TODO: more types + } + property.SetValue(OptionsWrapper.Options, value, null); + OptionsWrapper.SaveOptions(); + attr.Action().Invoke(s); + }); + } + + private static UISlider AddSlider(this UIHelperBase group, string text, string propertyName, SliderAttribute attr) + { + var property = typeof(T).GetProperty(propertyName); + UILabel valueLabel = null; + + var helper = group as UIHelper; + if (helper != null) + { + var type = typeof(UIHelper).GetField("m_Root", BindingFlags.NonPublic | BindingFlags.Instance); + if (type != null) + { + var panel = type.GetValue(helper) as UIComponent; + valueLabel = panel?.AddUIComponent(); + } + } + + float finalValue; + var value = property.GetValue(OptionsWrapper.Options, null); + if (value is float) + { + finalValue = (float)value; + } else if (value is byte) + { + finalValue = (byte) value; + } + else if (value is int) + { + finalValue = (int)value; + } + else + { + throw new Exception("Unsupported numeric type for slider!"); + } + + var slider = (UISlider)group.AddSlider(text, attr.Min, attr.Max, attr.Step, Mathf.Clamp(finalValue, attr.Min, attr.Max), + f => + { + if (value is float) + { + property.SetValue(OptionsWrapper.Options, f, null); + } + else if (value is byte) + { + property.SetValue(OptionsWrapper.Options, (byte)Math.Round(f, MidpointRounding.AwayFromZero), null); + } + else if (value is int) + { + property.SetValue(OptionsWrapper.Options, (int)Math.Round(f, MidpointRounding.AwayFromZero), null); + } + OptionsWrapper.SaveOptions(); + attr.Action().Invoke(f); + if (valueLabel != null) + { + valueLabel.text = f.ToString(CultureInfo.InvariantCulture); + } + }); + var nameLabel = slider.parent.Find("Label"); + if (nameLabel != null) + { + nameLabel.width = nameLabel.textScale * nameLabel.font.size * nameLabel.text.Length; + } + if (valueLabel == null) + { + return slider; + } + valueLabel.AlignTo(slider, UIAlignAnchor.TopLeft); + valueLabel.relativePosition = new Vector3(240, 0, 0); + valueLabel.text = value.ToString(); + return slider; + } + } +} \ No newline at end of file diff --git a/OptionsFramework.csproj b/OptionsFramework.csproj index 9536daa..2022061 100644 --- a/OptionsFramework.csproj +++ b/OptionsFramework.csproj @@ -30,15 +30,37 @@ 4 - - - - - - + + + + + + + + + + + + + + + - + + C:\Games\Steam\steamapps\common\Cities_Skylines\Cities_Data\Managed\Assembly-CSharp.dll + + + C:\Games\Steam\steamapps\common\Cities_Skylines\Cities_Data\Managed\ColossalManaged.dll + + + C:\Games\Steam\steamapps\common\Cities_Skylines\Cities_Data\Managed\ICities.dll + + + + + C:\Games\Steam\steamapps\common\Cities_Skylines\Cities_Data\Managed\UnityEngine.dll + \ No newline at end of file diff --git a/OptionsWrapper.cs b/OptionsWrapper.cs new file mode 100644 index 0000000..ca8a240 --- /dev/null +++ b/OptionsWrapper.cs @@ -0,0 +1,145 @@ +using System; +using System.IO; +using System.Xml; +using System.Xml.Serialization; +using ColossalFramework.IO; +using OptionsFramework.Attibutes; +using UnityEngine; + +namespace OptionsFramework +{ + public class OptionsWrapper + { + private static T _instance; + + public static T Options + { + get + { + try + { + Ensure(); + } + catch (XmlException e) + { + UnityEngine.Debug.LogError("Error reading options XML file"); + UnityEngine.Debug.LogException(e); + } + return _instance; + } + } + + public static void Ensure() + { + if (_instance != null) + { + return; + } + var type = typeof(T); + var attrs = type.GetCustomAttributes(typeof(OptionsAttribute), false); + if (attrs.Length != 1) + { + throw new Exception($"Type {type.FullName} is not an options type!"); + } + _instance = (T)Activator.CreateInstance(typeof(T)); + LoadOptions(); + } + + private static void LoadOptions() + { + try + { + if (GetLegacyFileName() != string.Empty) + { + try + { + ReadOptionsFile(GetLegacyFileName()); + try + { + File.Delete(GetLegacyFileName()); + } + catch (Exception e) + { + UnityEngine.Debug.LogException(e); + } + SaveOptions(); + } + catch (FileNotFoundException) + { + ReadOptionsFile(GetFileName()); + } + } + else + { + ReadOptionsFile(GetFileName()); + } + } + catch (FileNotFoundException) + { + SaveOptions();// No options file yet + } + } + + private static void ReadOptionsFile(string fileName) + { + var xmlSerializer = new XmlSerializer(typeof(T)); + using (var streamReader = new StreamReader(fileName)) + { + var options = (T) xmlSerializer.Deserialize(streamReader); + foreach (var propertyInfo in typeof(T).GetProperties()) + { + if (!propertyInfo.CanWrite) + { + continue; + } + var value = propertyInfo.GetValue(options, null); + propertyInfo.SetValue(_instance, value, null); + } + } + } + + internal static void SaveOptions() + { + try + { + var xmlSerializer = new XmlSerializer(typeof(T)); + using (var streamWriter = new StreamWriter(GetFileName())) + { + xmlSerializer.Serialize(streamWriter, _instance); + } + } + catch (Exception e) + { + Debug.LogException(e); + } + } + + private static string GetFileName() + { + var type = _instance.GetType(); + var attrs = type.GetCustomAttributes(typeof(OptionsAttribute), false); + var fileName = Path.Combine(DataLocation.localApplicationData, ((OptionsAttribute) attrs[0]).FileName); + if (!fileName.EndsWith(".xml")) + { + fileName = fileName + ".xml"; + } + return fileName; + } + + private static string GetLegacyFileName() + { + var type = _instance.GetType(); + var attrs = type.GetCustomAttributes(typeof(OptionsAttribute), false); + var fileName = ((OptionsAttribute)attrs[0]).LegacyFileName; + if (fileName == string.Empty) + { + return fileName; + } + if (!fileName.EndsWith(".xml")) + { + fileName = fileName + ".xml"; + } + return fileName; + } + } +} \ No newline at end of file diff --git a/Util.cs b/Util.cs new file mode 100644 index 0000000..1c31b7e --- /dev/null +++ b/Util.cs @@ -0,0 +1,28 @@ +using System; +using System.Linq; + +namespace OptionsFramework +{ + internal class Util + { + public static Type FindType(string className) + { + foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies()) + { + try + { + var types = assembly.GetTypes(); + foreach (var type in types.Where(type => type.Name == className)) + { + return type; + } + } + catch + { + // ignored + } + } + return null; + } + } +} \ No newline at end of file