├── Documentation~ ├── ChronoHelper-Manual.pdf └── index.md ├── CHANGELOG.md.meta ├── LICENSE.md.meta ├── README.md.meta ├── package.json.meta ├── Editor ├── dotsquid.ChronoHelper.Editor.asmdef.meta ├── Internal.meta ├── Internal │ ├── Data.meta │ ├── Utility.meta │ ├── PropertyDrawers.meta │ ├── Data │ │ ├── Consts.cs.meta │ │ ├── Enums.cs.meta │ │ ├── Settings.cs.meta │ │ ├── Base64Image.cs.meta │ │ ├── ChronoPoint.cs.meta │ │ ├── CustomPropertyList.cs.meta │ │ ├── Consts.cs │ │ ├── Enums.cs │ │ ├── CustomPropertyList.cs │ │ ├── ChronoPoint.cs │ │ ├── Settings.cs │ │ └── Base64Image.cs │ ├── Utility │ │ ├── GUIHelper.cs.meta │ │ ├── ChronoValueFormatter.cs.meta │ │ ├── GUIHelper.cs │ │ └── ChronoValueFormatter.cs │ ├── Helper.cs.meta │ ├── SettingsWindow.cs.meta │ ├── PropertyDrawers │ │ ├── BaseListDrawer.cs.meta │ │ ├── ChronoPointDrawer.cs.meta │ │ ├── ChronoPointListDrawer.cs.meta │ │ ├── ChronoPointListDrawer.cs │ │ ├── ChronoPointDrawer.cs │ │ └── BaseListDrawer.cs │ ├── Helper.cs │ └── SettingsWindow.cs ├── ChronoHelper.cs.meta ├── dotsquid.ChronoHelper.Editor.asmdef └── ChronoHelper.cs ├── Editor.meta ├── package.json ├── README.md ├── CHANGELOG.md └── LICENSE.md /Documentation~/ChronoHelper-Manual.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dotsquid/ChronoHelper/HEAD/Documentation~/ChronoHelper-Manual.pdf -------------------------------------------------------------------------------- /CHANGELOG.md.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: c4074ce3d7de71b4aa6fbaa870407730 3 | TextScriptImporter: 4 | externalObjects: {} 5 | userData: 6 | assetBundleName: 7 | assetBundleVariant: 8 | -------------------------------------------------------------------------------- /LICENSE.md.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 66f0ebd0fc3b28a4a84cb938bf32f816 3 | TextScriptImporter: 4 | externalObjects: {} 5 | userData: 6 | assetBundleName: 7 | assetBundleVariant: 8 | -------------------------------------------------------------------------------- /README.md.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 7b9f9bc381f9a584882b1b8c562f35c5 3 | TextScriptImporter: 4 | externalObjects: {} 5 | userData: 6 | assetBundleName: 7 | assetBundleVariant: 8 | -------------------------------------------------------------------------------- /package.json.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 1cc95ae4d61b1f54ba21b599fb3f6020 3 | TextScriptImporter: 4 | externalObjects: {} 5 | userData: 6 | assetBundleName: 7 | assetBundleVariant: 8 | -------------------------------------------------------------------------------- /Editor/dotsquid.ChronoHelper.Editor.asmdef.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: febacc5485953be4b9d38730ac1d1d37 3 | AssemblyDefinitionImporter: 4 | externalObjects: {} 5 | userData: 6 | assetBundleName: 7 | assetBundleVariant: 8 | -------------------------------------------------------------------------------- /Editor.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: c2d91cceb03562745b437204e83949f4 3 | folderAsset: yes 4 | timeCreated: 1520517199 5 | licenseType: Free 6 | DefaultImporter: 7 | userData: 8 | assetBundleName: 9 | assetBundleVariant: 10 | -------------------------------------------------------------------------------- /Editor/Internal.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: d04ca02e64346c3408a27c33011c1a9d 3 | folderAsset: yes 4 | timeCreated: 1520970625 5 | licenseType: Free 6 | DefaultImporter: 7 | userData: 8 | assetBundleName: 9 | assetBundleVariant: 10 | -------------------------------------------------------------------------------- /Editor/Internal/Data.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: a41d56ad08d70b9418c014411a98a977 3 | folderAsset: yes 4 | timeCreated: 1521056166 5 | licenseType: Free 6 | DefaultImporter: 7 | userData: 8 | assetBundleName: 9 | assetBundleVariant: 10 | -------------------------------------------------------------------------------- /Editor/Internal/Utility.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: cf348335977de3a4ba7b812996a1ee31 3 | folderAsset: yes 4 | timeCreated: 1521142211 5 | licenseType: Free 6 | DefaultImporter: 7 | userData: 8 | assetBundleName: 9 | assetBundleVariant: 10 | -------------------------------------------------------------------------------- /Editor/Internal/PropertyDrawers.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 801423970d58ce24b98444e6ee3470eb 3 | folderAsset: yes 4 | timeCreated: 1521056166 5 | licenseType: Free 6 | DefaultImporter: 7 | userData: 8 | assetBundleName: 9 | assetBundleVariant: 10 | -------------------------------------------------------------------------------- /Editor/Internal/Data/Consts.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: c876c8891f1e00f419af7849142a54db 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Editor/Internal/Utility/GUIHelper.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 59db24d8416c4e84f91b9295fe84f4e0 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Editor/ChronoHelper.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 23f2890164056bb4d9d6edee8df115fb 3 | timeCreated: 1520517199 4 | licenseType: Free 5 | MonoImporter: 6 | serializedVersion: 2 7 | defaultReferences: [] 8 | executionOrder: 0 9 | icon: {instanceID: 0} 10 | userData: 11 | assetBundleName: 12 | assetBundleVariant: 13 | -------------------------------------------------------------------------------- /Editor/Internal/Data/Enums.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 51adc880dd732f54bb98bd43c725e078 3 | timeCreated: 1521575284 4 | licenseType: Free 5 | MonoImporter: 6 | serializedVersion: 2 7 | defaultReferences: [] 8 | executionOrder: 0 9 | icon: {instanceID: 0} 10 | userData: 11 | assetBundleName: 12 | assetBundleVariant: 13 | -------------------------------------------------------------------------------- /Editor/Internal/Helper.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 4f99b12fc9cb2e040811a08f0b514565 3 | timeCreated: 1520970625 4 | licenseType: Free 5 | MonoImporter: 6 | serializedVersion: 2 7 | defaultReferences: [] 8 | executionOrder: 0 9 | icon: {instanceID: 0} 10 | userData: 11 | assetBundleName: 12 | assetBundleVariant: 13 | -------------------------------------------------------------------------------- /Editor/Internal/Data/Settings.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: c9c5ba6c7f639a24282c78313884cb55 3 | timeCreated: 1521411569 4 | licenseType: Free 5 | MonoImporter: 6 | serializedVersion: 2 7 | defaultReferences: [] 8 | executionOrder: 0 9 | icon: {instanceID: 0} 10 | userData: 11 | assetBundleName: 12 | assetBundleVariant: 13 | -------------------------------------------------------------------------------- /Editor/Internal/SettingsWindow.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 3563a8ece13f18a4bac4cdb905a4d859 3 | timeCreated: 1520970625 4 | licenseType: Free 5 | MonoImporter: 6 | serializedVersion: 2 7 | defaultReferences: [] 8 | executionOrder: 0 9 | icon: {instanceID: 0} 10 | userData: 11 | assetBundleName: 12 | assetBundleVariant: 13 | -------------------------------------------------------------------------------- /Editor/Internal/Data/Base64Image.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 87bccb47c7ce25b428839dc90b14d95c 3 | timeCreated: 1522003964 4 | licenseType: Free 5 | MonoImporter: 6 | serializedVersion: 2 7 | defaultReferences: [] 8 | executionOrder: 0 9 | icon: {instanceID: 0} 10 | userData: 11 | assetBundleName: 12 | assetBundleVariant: 13 | -------------------------------------------------------------------------------- /Editor/Internal/Data/ChronoPoint.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 674bbf4430336b54087609c6d7e99f44 3 | timeCreated: 1521057677 4 | licenseType: Free 5 | MonoImporter: 6 | serializedVersion: 2 7 | defaultReferences: [] 8 | executionOrder: 0 9 | icon: {instanceID: 0} 10 | userData: 11 | assetBundleName: 12 | assetBundleVariant: 13 | -------------------------------------------------------------------------------- /Editor/Internal/Data/CustomPropertyList.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 26d4669bae321ee4781d548152352b34 3 | timeCreated: 1521056166 4 | licenseType: Free 5 | MonoImporter: 6 | serializedVersion: 2 7 | defaultReferences: [] 8 | executionOrder: 0 9 | icon: {instanceID: 0} 10 | userData: 11 | assetBundleName: 12 | assetBundleVariant: 13 | -------------------------------------------------------------------------------- /Editor/Internal/PropertyDrawers/BaseListDrawer.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 1b738215965ab364499c24653ec5bfa5 3 | timeCreated: 1521056166 4 | licenseType: Free 5 | MonoImporter: 6 | serializedVersion: 2 7 | defaultReferences: [] 8 | executionOrder: 0 9 | icon: {instanceID: 0} 10 | userData: 11 | assetBundleName: 12 | assetBundleVariant: 13 | -------------------------------------------------------------------------------- /Editor/Internal/Utility/ChronoValueFormatter.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: fe6ef52f88a0ecd43b6273de09018963 3 | timeCreated: 1521145473 4 | licenseType: Free 5 | MonoImporter: 6 | serializedVersion: 2 7 | defaultReferences: [] 8 | executionOrder: 0 9 | icon: {instanceID: 0} 10 | userData: 11 | assetBundleName: 12 | assetBundleVariant: 13 | -------------------------------------------------------------------------------- /Editor/Internal/PropertyDrawers/ChronoPointDrawer.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: ed5ec7dbc9c2715499bd41a422efec78 3 | timeCreated: 1521059554 4 | licenseType: Free 5 | MonoImporter: 6 | serializedVersion: 2 7 | defaultReferences: [] 8 | executionOrder: 0 9 | icon: {instanceID: 0} 10 | userData: 11 | assetBundleName: 12 | assetBundleVariant: 13 | -------------------------------------------------------------------------------- /Editor/Internal/PropertyDrawers/ChronoPointListDrawer.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: ed36c0c87277ef94fb75c3ca35058c65 3 | timeCreated: 1521057677 4 | licenseType: Free 5 | MonoImporter: 6 | serializedVersion: 2 7 | defaultReferences: [] 8 | executionOrder: 0 9 | icon: {instanceID: 0} 10 | userData: 11 | assetBundleName: 12 | assetBundleVariant: 13 | -------------------------------------------------------------------------------- /Documentation~/index.md: -------------------------------------------------------------------------------- 1 | # Chrono Helper 2 | 3 | ChronoHelper is a free open-source tool for Unity Editor for controlling TimeScale in PlayMode with ease. 4 | It becomes very handy when it’s required to examine a suspicious moment of gameplay in slow-motion or conversely when it’s preferable to skip uninteresting part in fast-forward. 5 | 6 | [ChronoHelper Manual](ChronoHelper-Manual.pdf). 7 | -------------------------------------------------------------------------------- /Editor/dotsquid.ChronoHelper.Editor.asmdef: -------------------------------------------------------------------------------- 1 | { 2 | "name": "dotsquid.ChronoHelper.Editor", 3 | "references": [], 4 | "optionalUnityReferences": [], 5 | "includePlatforms": [ 6 | "Editor" 7 | ], 8 | "excludePlatforms": [], 9 | "allowUnsafeCode": false, 10 | "overrideReferences": false, 11 | "precompiledReferences": [], 12 | "autoReferenced": true, 13 | "defineConstraints": [] 14 | } -------------------------------------------------------------------------------- /Editor/Internal/Data/Consts.cs: -------------------------------------------------------------------------------- 1 | namespace dotsquid.ChronoHelper.Internal 2 | { 3 | internal static class Consts 4 | { 5 | public const string kName = "com.dotsquid.ChronoHelper"; 6 | public const string kNamePrefix = kName + "."; 7 | 8 | public static class URL 9 | { 10 | public const string Github = "https://github.com/dotsquid/ChronoHelper"; 11 | public const string Homepage = "http://dotsquid.com/works/chrono-helper/"; 12 | public const string DotsquidDotCom = "http://dotsquid.com"; 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /Editor/Internal/Data/Enums.cs: -------------------------------------------------------------------------------- 1 | namespace dotsquid.ChronoHelper.Internal 2 | { 3 | internal enum Format 4 | { 5 | AsIs, 6 | Short, 7 | Compact 8 | } 9 | 10 | internal enum ButtonWidth 11 | { 12 | Equal, 13 | AsIs, 14 | } 15 | 16 | internal enum WarningMode 17 | { 18 | Never, 19 | WhenNotSuppressing, 20 | Always, 21 | } 22 | 23 | internal enum Layout 24 | { 25 | Auto, 26 | Vertical, 27 | Horizontal, 28 | } 29 | 30 | internal enum BlockOrder 31 | { 32 | Normal, 33 | Reversed, 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /Editor/Internal/Utility/GUIHelper.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using UnityEngine; 3 | 4 | namespace dotsquid.ChronoHelper.Internal 5 | { 6 | internal static class GUIHelper 7 | { 8 | public struct ReplaceColor : IDisposable 9 | { 10 | public static ReplaceColor With(Color color) => new ReplaceColor(color); 11 | 12 | private Color _oldColor; 13 | 14 | private ReplaceColor(Color color) 15 | { 16 | _oldColor = GUI.color; 17 | GUI.color = color; 18 | } 19 | 20 | void IDisposable.Dispose() => GUI.color = _oldColor; 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "com.dotsquid.chronohelper", 3 | "displayName": "ChronoHelper", 4 | "version": "2.0.0", 5 | "unity": "2018.1", 6 | "description": "ChronoHelper is a free open-source tool for Unity Editor for controlling TimeScale in PlayMode with ease.\nIt becomes very handy when it’s required to examine a suspicious moment of gameplay in slow-motion or conversely when it’s preferable to skip uninteresting part in fast-forward.", 7 | "keywords": [ 8 | "ChronoHelper", 9 | "Time", 10 | "TimeScale" 11 | ], 12 | "author": { 13 | "name": "dotsquid", 14 | "email": "dotsquid@gmail.com", 15 | "url": "http://dotsquid.com" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /Editor/Internal/Data/CustomPropertyList.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using UnityEngine; 4 | 5 | namespace dotsquid.ChronoHelper.Internal 6 | { 7 | [Serializable] 8 | internal class CustomPropertyList 9 | { 10 | public const string kListFieldName = "_list"; 11 | } 12 | 13 | [Serializable] 14 | internal class CustomPropertyArray : CustomPropertyList 15 | { 16 | [SerializeField] 17 | protected T[] _list = new T[0]; 18 | 19 | public T[] list => _list; 20 | 21 | public void CopyFrom(CustomPropertyArray other) 22 | { 23 | var otherList = other.list; 24 | int count = otherList.Length; 25 | _list = new T[count]; 26 | other.list.CopyTo(_list, 0); 27 | } 28 | 29 | public void CopyFrom(T[] other) 30 | { 31 | int count = other.Length; 32 | _list = new T[count]; 33 | other.CopyTo(_list, 0); 34 | } 35 | } 36 | 37 | [Serializable] 38 | internal class CustomPropertyList : CustomPropertyList 39 | { 40 | [SerializeField] 41 | protected List _list = new List(); 42 | 43 | public List list => _list; 44 | public T[] array => _list.ToArray(); 45 | 46 | public void Add(T item) => _list.Add(item); 47 | public bool Remove(T item) => _list.Remove(item); 48 | public bool Contains(T item) => _list.IndexOf(item) > -1; 49 | } 50 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Logo+Title](http://dotsquid.com/images/products/ChronoHelper/ChronoHelper_Title-1536.png) 2 | 3 | ChronoHelper is a free open-source tool for Unity Editor for controlling TimeScale in PlayMode with ease. 4 | It becomes very handy when it’s required to examine a suspicious moment of gameplay in slow-motion or conversely when it’s preferable to skip uninteresting part in fast-forward. 5 | 6 | ## Usage 7 | Use ‘Window/ChronoHelper’ menu to open ChronoHelper. 8 | To change current timeScale use the slider or shortcut buttons. 9 | While being in EditorMode, ChronoHelper is inactive. That is done to protect the user from accidental changing of Time.timeScale project setting. 10 | ![Usage](https://i.imgur.com/wyETLir.gif) 11 | 12 | You can add, remove and modify shortcut buttons as well as change other preferences in the Settings window. 13 | ![Settings](https://i.imgur.com/mdjLvcw.gif) 14 | 15 | ## Installation 16 | ### Package Manager (preferred) 17 | To add ChronoHelper as a package: 18 | 1) go to *Window/Package Manager*; 19 | 2) press '➕▾' button (in the top left corner of the window); 20 | 3) select 'Add package from git URL'; 21 | 4) insert URL of this repository *https://github.com/dotsquid/ChronoHelper.git*. 22 | 23 | ### Git submodule 24 | Open your favourite command-line interface, switch to your project's directory and use the following command 25 | `git submodule add https://github.com/dotsquid/ChronoHelper` 26 | Or follow the documentation of your preferred Git-client. 27 | 28 | ### AssetStore 29 | Download and install from *https://assetstore.unity.com/packages/tools/utilities/chronohelper-116665* 30 | 31 | ### Old way (not recommended) 32 | Download *ChronoHelper* as a ZIP-archive and unpack it to your project's *'Asset'* folder. 33 | 34 | ## Documentation 35 | You may find more information [in the manual](Documentation~/ChronoHelper-Manual.pdf) 36 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ChangeLog 2 | ========= 3 | 4 | 2.0.0 (2020-12-27) 5 | ------------------ 6 | #### Implemented enhancements: 7 | * Added support of arbitrary number of 'ChronoButtons'. 8 | * Added settings for layout (horizontal, vertical) and block order. 9 | * Added customizable warning intended to notify when timeScale is being change from outside (e.g. script). 10 | * Label style of 'ChronoButtons' is now customizable (AsIs, Short, Compact). 11 | * Other minor UI improvements. 12 | 13 | 1.4.0 (2018-01-14) 14 | ------------------ 15 | #### Implemented enhancements: 16 | * Improved UI; added support of Dark/Pro skin. 17 | 18 | 1.3.0 (2018-01-14) 19 | ------------------ 20 | #### Fixed bugs: 21 | * Missing window in the layout after Editor restart. 22 | * Storing / restoring / applying chronoScale when opening / closing window and switching between play / edit mode. 23 | 24 | 1.2.1 (2017-08-06) 25 | ------------------ 26 | #### Fixed bugs: 27 | * Doing nothing if opened in PlayMode. 28 | 29 | 1.2.0 (2017-04-10) 30 | ------------------ 31 | #### Implemented enhancements: 32 | * Added saving and loading ChronoHelper's state thus when ChronoHelper is opened again it restores its previous state. This relates to chronoScale, canResetOnPlayEnd and canSuppressTimeScale. 33 | 34 | 1.1.0 (2017-04-09) 35 | ------------------ 36 | #### Fixed bugs: 37 | * Problem with chronoScale reset to wrong value when switching from PlayMode back to EditMode 38 | 39 | #### Implemented enhancements: 40 | * chronoScale can be preset in EditMode. It will be applied to Time.timeScale on entering to PlayMode 41 | * Correct tooltips accordingly to other changes. 42 | * Added "Auto-reset chronoScale to value set in EditMode" toggle button. If set chronoScale will be reset to initial value which was set in EditMode. Otherwise it will keep its last value set in PlayMode. 43 | * Added "Suppress Time.timeScale changes from without" toggle button. If set any changes made to Time.timeScale in PlayMode (either from script or from ProjectSettings > Time) will be suppressed and overriden with chronoScale. Otherwise chronoScale will catch up changes made to Time.timeScale. 44 | 45 | 46 | 1.0.0 (2017-01-12) 47 | ------------------ 48 | Initial working version -------------------------------------------------------------------------------- /Editor/Internal/PropertyDrawers/ChronoPointListDrawer.cs: -------------------------------------------------------------------------------- 1 | using UnityEditor; 2 | using UnityEngine; 3 | 4 | namespace dotsquid.ChronoHelper.Internal 5 | { 6 | [CustomPropertyDrawer(typeof(ChronoPointList), true)] 7 | internal class ChronoPointListDrawer : BaseListDrawer 8 | { 9 | private const string kHeader = "Shortcut buttons"; 10 | private const float kHeightEnlargement = 2.0f; 11 | 12 | protected override string header => kHeader; 13 | 14 | public override void OnGUI(Rect position, SerializedProperty property, GUIContent label) 15 | { 16 | base.OnGUI(position, property, label); 17 | _list.displayRemove = false; 18 | _list.draggable = false; 19 | } 20 | 21 | protected override void OnChanged(SerializedProperty element, int index) 22 | { 23 | Validate(element); 24 | Sort(); 25 | } 26 | 27 | protected override bool HasRemoveButton(SerializedProperty element, int index) 28 | { 29 | return !IsReadOnly(element); 30 | } 31 | 32 | protected override float GetElementHeight(int index) 33 | { 34 | return base.GetElementHeight(index) + kHeightEnlargement; 35 | } 36 | 37 | private bool IsReadOnly(SerializedProperty element) 38 | { 39 | if (element != null) 40 | { 41 | var prop = element.FindPropertyRelative(ChronoPoint.kIsReadOnlyPropName); 42 | if (prop != null) 43 | { 44 | return prop.boolValue; 45 | } 46 | } 47 | return true; 48 | } 49 | 50 | private void Validate(SerializedProperty element) 51 | { 52 | if (element != null) 53 | { 54 | var prop = element.FindPropertyRelative(ChronoPoint.kValuePropName); 55 | if (prop != null) 56 | { 57 | prop.floatValue = ChronoPoint.ValidateValue(prop.floatValue); 58 | } 59 | } 60 | } 61 | 62 | private void Sort() 63 | { 64 | var settings = (_property.serializedObject.targetObject as Settings); 65 | var list = settings.chronoPointList; 66 | if (list != null) 67 | { 68 | list.SortByValue(); 69 | _list.serializedProperty.serializedObject.Update(); 70 | } 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /Editor/Internal/Helper.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using UnityEngine; 3 | 4 | namespace dotsquid.ChronoHelper.Internal 5 | { 6 | internal static class Helper 7 | { 8 | public static Texture2D CreateTextureFromBase64(string base64, string name = "") 9 | { 10 | byte[] data = Convert.FromBase64String(base64); 11 | var tex = new Texture2D(1, 1, TextureFormat.ARGB32, false, true) 12 | { 13 | hideFlags = HideFlags.HideAndDontSave, 14 | name = name, 15 | filterMode = FilterMode.Bilinear 16 | }; 17 | tex.LoadImage(data); 18 | return tex; 19 | } 20 | 21 | public struct Version 22 | { 23 | public enum State 24 | { 25 | Alpha, 26 | Beta, 27 | ReleaseCandidate, 28 | Final, 29 | } 30 | 31 | private int _major; 32 | private int _minor; 33 | private int _patch; 34 | private string _formatted; 35 | 36 | public Version(int major, int minor, int patch, State state) 37 | { 38 | _major = major; 39 | _minor = minor; 40 | _patch = patch; 41 | _formatted = $"{_major}.{_minor}.{_patch}{GetStateName(state)}"; 42 | } 43 | 44 | public static implicit operator string(Version self) 45 | { 46 | return self.ToString(); 47 | } 48 | 49 | public override string ToString() 50 | { 51 | if (string.IsNullOrEmpty(_formatted)) 52 | throw new NotImplementedException("Can't be used with default value"); 53 | return _formatted; 54 | } 55 | 56 | private static string GetStateName(State state) 57 | { 58 | switch (state) 59 | { 60 | case State.Alpha: 61 | return "a"; 62 | case State.Beta: 63 | return "b"; 64 | case State.ReleaseCandidate: 65 | return "rc"; 66 | case State.Final: 67 | return "f"; 68 | default: 69 | return ""; 70 | } 71 | } 72 | } 73 | 74 | public static readonly Version version = new Version(2, 0, 0, Version.State.Final); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /Editor/Internal/Data/ChronoPoint.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using UnityEngine; 6 | 7 | namespace dotsquid.ChronoHelper.Internal 8 | { 9 | [Serializable] 10 | internal struct ChronoPoint : ISerializationCallbackReceiver 11 | { 12 | public const string kValuePropName = nameof(_value); 13 | public const string kCustomDisplayPropName = nameof(_customDisplay); 14 | public const string kIsReadOnlyPropName = nameof(_isReadOnly); 15 | 16 | public static ChronoPoint Default = new ChronoPoint() 17 | { 18 | _value = 1.0f 19 | }; 20 | 21 | [SerializeField] 22 | private float _value; 23 | [SerializeField, HideInInspector] 24 | private string _customDisplay; 25 | [SerializeField, HideInInspector] 26 | private bool _isReadOnly; 27 | 28 | public float value 29 | { 30 | get { return _value; } 31 | set { _value = ValidateValue(value); } 32 | } 33 | 34 | public bool isReadOnly => _isReadOnly; 35 | 36 | public ChronoPoint(float value, string customDisplay = null) 37 | { 38 | _value = value; 39 | _customDisplay = customDisplay; 40 | _isReadOnly = (null != _customDisplay); 41 | } 42 | 43 | public ChronoPoint(float value, bool isReadOnly) 44 | { 45 | _value = value; 46 | _customDisplay = null; 47 | _isReadOnly = isReadOnly; 48 | } 49 | 50 | public static float ValidateValue(float value) 51 | { 52 | return Math.Max(value, 0.0f); 53 | } 54 | 55 | void ISerializationCallbackReceiver.OnAfterDeserialize() 56 | { 57 | _value = ValidateValue(_value); 58 | } 59 | 60 | void ISerializationCallbackReceiver.OnBeforeSerialize() 61 | { } 62 | } 63 | 64 | [Serializable] 65 | internal class ChronoPointList : CustomPropertyList, IEnumerable 66 | { 67 | public ChronoPointList() : base() 68 | { } 69 | 70 | public ChronoPointList(IEnumerable collection) : base() 71 | { 72 | _list = new List(collection); 73 | } 74 | 75 | public IEnumerator GetEnumerator() 76 | { 77 | return _list.GetEnumerator(); 78 | } 79 | 80 | public void SortByValue() 81 | { 82 | _list = _list.OrderBy(point => point.value).ThenBy(point => !point.isReadOnly).ToList(); 83 | } 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /Editor/Internal/PropertyDrawers/ChronoPointDrawer.cs: -------------------------------------------------------------------------------- 1 | using UnityEditor; 2 | using UnityEngine; 3 | 4 | namespace dotsquid.ChronoHelper.Internal 5 | { 6 | [CustomPropertyDrawer(typeof(ChronoPoint), true)] 7 | internal class ChronoPointDrawer : PropertyDrawer 8 | { 9 | private class Styles 10 | { 11 | public GUIStyle chronoValueField; 12 | } 13 | 14 | private const float kMarginWidth = 8.0f; 15 | private const float kGapWidth = 16.0f; 16 | private const float kValueRectWidth = 64.0f; 17 | private const float kHeightReduction = 4.0f; 18 | 19 | private Styles _styles; 20 | 21 | public override void OnGUI(Rect position, SerializedProperty property, GUIContent label) 22 | { 23 | EnsureStyles(); 24 | DrawPoint(position, property); 25 | } 26 | 27 | private void DrawPoint(Rect position, SerializedProperty property) 28 | { 29 | var isReadOnlyProp = property.FindPropertyRelative(ChronoPoint.kIsReadOnlyPropName); 30 | var valueProp = property.FindPropertyRelative(ChronoPoint.kValuePropName); 31 | 32 | bool isReadOnly = isReadOnlyProp != null 33 | ? isReadOnlyProp.boolValue 34 | : false; 35 | 36 | position.y += kHeightReduction * 0.5f; 37 | position.height -= kHeightReduction; 38 | float valueRectWidth = position.width - kValueRectWidth - kGapWidth - kMarginWidth; 39 | var valueRect = new Rect(position) 40 | { 41 | width = kValueRectWidth, 42 | x = position.x + kMarginWidth 43 | }; 44 | var displayRect = new Rect(position) 45 | { 46 | width = valueRectWidth, 47 | x = valueRect.x + kValueRectWidth + kGapWidth 48 | }; 49 | 50 | using (var group = new EditorGUI.DisabledGroupScope(isReadOnly)) 51 | { 52 | float value = valueProp.floatValue; 53 | string displayValue = GetDisplayValue(value); 54 | valueProp.floatValue = EditorGUI.DelayedFloatField(valueRect, GUIContent.none, value, _styles.chronoValueField); 55 | EditorGUI.LabelField(displayRect, displayValue); 56 | } 57 | } 58 | 59 | private void EnsureStyles() 60 | { 61 | if (_styles == null) 62 | { 63 | _styles = new Styles(); 64 | _styles.chronoValueField = new GUIStyle(GUI.skin.textField); 65 | _styles.chronoValueField.alignment = TextAnchor.MiddleRight; 66 | } 67 | } 68 | 69 | private static string GetDisplayValue(float value) 70 | { 71 | if (Mathf.Approximately(value, 0.0f)) 72 | return "▍▍ (paused)"; 73 | else if(Mathf.Approximately(value, 1.0f)) 74 | return "×1 (normal)"; 75 | return ChronoValueFormatter.Nicify(value, Settings.I.buttonFormat); 76 | } 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /Editor/Internal/Data/Settings.cs: -------------------------------------------------------------------------------- 1 | using UnityEngine; 2 | using UnityEditor; 3 | 4 | namespace dotsquid.ChronoHelper.Internal 5 | { 6 | internal class Settings : ScriptableObject 7 | { 8 | private static readonly Color kDefaultNormalBackColor = new Color(1.0f, 1.0f, 1.0f, 0.1f); 9 | private static readonly Color kDefaultWarningBackColor = new Color32(212, 77, 246, 225); 10 | private const string kEditorPrefName = Consts.kNamePrefix + "settings"; 11 | private const float kDefaultWarningBlinkPeriod = 2.0f; 12 | private const float kDefaultWarningBlinkDuration = 4.0f; 13 | 14 | public static Settings I 15 | { 16 | get 17 | { 18 | if (_instance == null) 19 | { 20 | Load(); 21 | } 22 | return _instance; 23 | } 24 | } 25 | 26 | #region Stored data 27 | public ChronoPointList chronoPointList = new ChronoPointList() 28 | { 29 | new ChronoPoint(0.0f, true), 30 | new ChronoPoint(0.125f), 31 | new ChronoPoint(0.25f), 32 | new ChronoPoint(0.5f), 33 | new ChronoPoint(1.0f, true), 34 | new ChronoPoint(1.5f), 35 | new ChronoPoint(2.0f) 36 | }; 37 | public Layout layout = Layout.Auto; 38 | public BlockOrder blockOrder = BlockOrder.Normal; 39 | public ButtonWidth buttonWidth = ButtonWidth.Equal; 40 | public Format buttonFormat = Format.Compact; 41 | public WarningMode warningMode = WarningMode.WhenNotSuppressing; 42 | public bool showResetButton = true; 43 | public Color normalBackColor = kDefaultNormalBackColor; 44 | public Color warningBackColor = kDefaultWarningBackColor; 45 | public float warningBlinkPeriod = kDefaultWarningBlinkPeriod; 46 | public float warningBlinkDuration = kDefaultWarningBlinkDuration; 47 | #endregion 48 | 49 | private static Settings _instance; 50 | private SerializedObject _serializedObject; 51 | private SerializedProperty _chronoPointsListProp; 52 | 53 | public SerializedObject serializedObject => _serializedObject; 54 | public SerializedProperty chronoPointsListProp => _chronoPointsListProp; 55 | 56 | public void ResetToDefault() 57 | { 58 | _instance = CreateInstance(); 59 | _instance.Init(); 60 | } 61 | 62 | public void Save() 63 | { 64 | var json = JsonUtility.ToJson(_instance, false); 65 | EditorPrefs.SetString(kEditorPrefName, json); 66 | } 67 | 68 | private static void Load() 69 | { 70 | var json = EditorPrefs.GetString(kEditorPrefName); 71 | _instance = CreateInstance(); 72 | try 73 | { 74 | JsonUtility.FromJsonOverwrite(json, _instance); 75 | } 76 | catch 77 | { 78 | _instance = CreateInstance(); 79 | } 80 | _instance.Init(); 81 | } 82 | 83 | private void Init() 84 | { 85 | _serializedObject = new SerializedObject(this); 86 | _chronoPointsListProp = _serializedObject.FindProperty(nameof(chronoPointList)); 87 | } 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /Editor/Internal/Utility/ChronoValueFormatter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace dotsquid.ChronoHelper.Internal 4 | { 5 | internal static class ChronoValueFormatter 6 | { 7 | private struct Fraction 8 | { 9 | public float value; 10 | public string glyph; 11 | 12 | public Fraction(float value, string glyph) 13 | { 14 | this.value = value; 15 | this.glyph = glyph; 16 | } 17 | } 18 | 19 | private static readonly Fraction[] kFractions = new Fraction[] 20 | { 21 | new Fraction(0.000f, ""), 22 | new Fraction(0.100f, "⅒"), 23 | new Fraction(0.111f, "⅑"), 24 | new Fraction(0.125f, "⅛"), 25 | new Fraction(0.143f, "⅐"), 26 | new Fraction(0.167f, "⅙"), 27 | new Fraction(0.200f, "⅕"), 28 | new Fraction(0.250f, "¼"), 29 | new Fraction(0.333f, "⅓"), 30 | new Fraction(0.375f, "⅜"), 31 | new Fraction(0.400f, "⅖"), 32 | new Fraction(0.500f, "½"), 33 | new Fraction(0.600f, "⅗"), 34 | new Fraction(0.625f, "⅝"), 35 | new Fraction(0.667f, "⅔"), 36 | new Fraction(0.750f, "¾"), 37 | new Fraction(0.800f, "⅘"), 38 | new Fraction(0.833f, "⅚"), 39 | new Fraction(0.875f, "⅞"), 40 | new Fraction(1.000f, ""), 41 | }; 42 | 43 | private const string kChronoValuePrefix = "×"; 44 | 45 | public static string Nicify(float value, Format mode) 46 | { 47 | switch (mode) 48 | { 49 | case Format.Compact: 50 | return GetChronoValueCompact(value); 51 | 52 | case Format.Short: 53 | return GetChronoValueShort(value); 54 | 55 | case Format.AsIs: 56 | default: 57 | return GetChronoValueAsIs(value); 58 | } 59 | } 60 | 61 | private static string GetChronoValueAsIs(float value) 62 | { 63 | return $"{kChronoValuePrefix}{value}"; 64 | } 65 | 66 | private static string GetChronoValueShort(float value) 67 | { 68 | return $"{kChronoValuePrefix}{value:0.#}"; 69 | } 70 | 71 | private static string GetChronoValueCompact(float value) 72 | { 73 | int addition = GetFractionGlyph(value, out var fractionGlyph); 74 | int integral = (int)Math.Truncate(value) + addition; 75 | string integralGlyph = ((integral != 0) || string.IsNullOrEmpty(fractionGlyph)) 76 | ? integral.ToString() 77 | : null; 78 | return $"{kChronoValuePrefix}{integralGlyph}{fractionGlyph}"; 79 | } 80 | 81 | private static int GetFractionGlyph(float value, out string glyph) 82 | { 83 | float fraction = value - (float)Math.Truncate(value); 84 | var result = default(Fraction); 85 | for (int i = 0, count = kFractions.Length - 1; i < count; ++i) 86 | { 87 | var leftFraction = kFractions[i]; 88 | var rightFraction = kFractions[i + 1]; 89 | var leftValue = leftFraction.value; 90 | var rightValue = rightFraction.value; 91 | if (fraction >= leftValue && fraction <= rightValue) 92 | { 93 | if (fraction - leftValue < rightValue - fraction) 94 | result = leftFraction; 95 | else 96 | result = rightFraction; 97 | } 98 | } 99 | glyph = result.glyph; 100 | return (int)Math.Floor(result.value); 101 | } 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Creative Commons Legal Code 2 | 3 | CC0 1.0 Universal 4 | 5 | CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE 6 | LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN 7 | ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS 8 | INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES 9 | REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS 10 | PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM 11 | THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED 12 | HEREUNDER. 13 | 14 | Statement of Purpose 15 | 16 | The laws of most jurisdictions throughout the world automatically confer 17 | exclusive Copyright and Related Rights (defined below) upon the creator 18 | and subsequent owner(s) (each and all, an "owner") of an original work of 19 | authorship and/or a database (each, a "Work"). 20 | 21 | Certain owners wish to permanently relinquish those rights to a Work for 22 | the purpose of contributing to a commons of creative, cultural and 23 | scientific works ("Commons") that the public can reliably and without fear 24 | of later claims of infringement build upon, modify, incorporate in other 25 | works, reuse and redistribute as freely as possible in any form whatsoever 26 | and for any purposes, including without limitation commercial purposes. 27 | These owners may contribute to the Commons to promote the ideal of a free 28 | culture and the further production of creative, cultural and scientific 29 | works, or to gain reputation or greater distribution for their Work in 30 | part through the use and efforts of others. 31 | 32 | For these and/or other purposes and motivations, and without any 33 | expectation of additional consideration or compensation, the person 34 | associating CC0 with a Work (the "Affirmer"), to the extent that he or she 35 | is an owner of Copyright and Related Rights in the Work, voluntarily 36 | elects to apply CC0 to the Work and publicly distribute the Work under its 37 | terms, with knowledge of his or her Copyright and Related Rights in the 38 | Work and the meaning and intended legal effect of CC0 on those rights. 39 | 40 | 1. Copyright and Related Rights. A Work made available under CC0 may be 41 | protected by copyright and related or neighboring rights ("Copyright and 42 | Related Rights"). Copyright and Related Rights include, but are not 43 | limited to, the following: 44 | 45 | i. the right to reproduce, adapt, distribute, perform, display, 46 | communicate, and translate a Work; 47 | ii. moral rights retained by the original author(s) and/or performer(s); 48 | iii. publicity and privacy rights pertaining to a person's image or 49 | likeness depicted in a Work; 50 | iv. rights protecting against unfair competition in regards to a Work, 51 | subject to the limitations in paragraph 4(a), below; 52 | v. rights protecting the extraction, dissemination, use and reuse of data 53 | in a Work; 54 | vi. database rights (such as those arising under Directive 96/9/EC of the 55 | European Parliament and of the Council of 11 March 1996 on the legal 56 | protection of databases, and under any national implementation 57 | thereof, including any amended or successor version of such 58 | directive); and 59 | vii. other similar, equivalent or corresponding rights throughout the 60 | world based on applicable law or treaty, and any national 61 | implementations thereof. 62 | 63 | 2. Waiver. To the greatest extent permitted by, but not in contravention 64 | of, applicable law, Affirmer hereby overtly, fully, permanently, 65 | irrevocably and unconditionally waives, abandons, and surrenders all of 66 | Affirmer's Copyright and Related Rights and associated claims and causes 67 | of action, whether now known or unknown (including existing as well as 68 | future claims and causes of action), in the Work (i) in all territories 69 | worldwide, (ii) for the maximum duration provided by applicable law or 70 | treaty (including future time extensions), (iii) in any current or future 71 | medium and for any number of copies, and (iv) for any purpose whatsoever, 72 | including without limitation commercial, advertising or promotional 73 | purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each 74 | member of the public at large and to the detriment of Affirmer's heirs and 75 | successors, fully intending that such Waiver shall not be subject to 76 | revocation, rescission, cancellation, termination, or any other legal or 77 | equitable action to disrupt the quiet enjoyment of the Work by the public 78 | as contemplated by Affirmer's express Statement of Purpose. 79 | 80 | 3. Public License Fallback. Should any part of the Waiver for any reason 81 | be judged legally invalid or ineffective under applicable law, then the 82 | Waiver shall be preserved to the maximum extent permitted taking into 83 | account Affirmer's express Statement of Purpose. In addition, to the 84 | extent the Waiver is so judged Affirmer hereby grants to each affected 85 | person a royalty-free, non transferable, non sublicensable, non exclusive, 86 | irrevocable and unconditional license to exercise Affirmer's Copyright and 87 | Related Rights in the Work (i) in all territories worldwide, (ii) for the 88 | maximum duration provided by applicable law or treaty (including future 89 | time extensions), (iii) in any current or future medium and for any number 90 | of copies, and (iv) for any purpose whatsoever, including without 91 | limitation commercial, advertising or promotional purposes (the 92 | "License"). The License shall be deemed effective as of the date CC0 was 93 | applied by Affirmer to the Work. Should any part of the License for any 94 | reason be judged legally invalid or ineffective under applicable law, such 95 | partial invalidity or ineffectiveness shall not invalidate the remainder 96 | of the License, and in such case Affirmer hereby affirms that he or she 97 | will not (i) exercise any of his or her remaining Copyright and Related 98 | Rights in the Work or (ii) assert any associated claims and causes of 99 | action with respect to the Work, in either case contrary to Affirmer's 100 | express Statement of Purpose. 101 | 102 | 4. Limitations and Disclaimers. 103 | 104 | a. No trademark or patent rights held by Affirmer are waived, abandoned, 105 | surrendered, licensed or otherwise affected by this document. 106 | b. Affirmer offers the Work as-is and makes no representations or 107 | warranties of any kind concerning the Work, express, implied, 108 | statutory or otherwise, including without limitation warranties of 109 | title, merchantability, fitness for a particular purpose, non 110 | infringement, or the absence of latent or other defects, accuracy, or 111 | the present or absence of errors, whether or not discoverable, all to 112 | the greatest extent permissible under applicable law. 113 | c. Affirmer disclaims responsibility for clearing rights of other persons 114 | that may apply to the Work or any use thereof, including without 115 | limitation any person's Copyright and Related Rights in the Work. 116 | Further, Affirmer disclaims responsibility for obtaining any necessary 117 | consents, permissions or other rights required for any use of the 118 | Work. 119 | d. Affirmer understands and acknowledges that Creative Commons is not a 120 | party to this document and has no duty or obligation with respect to 121 | this CC0 or use of the Work. 122 | -------------------------------------------------------------------------------- /Editor/Internal/SettingsWindow.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using UnityEditor; 3 | using UnityEngine; 4 | 5 | namespace dotsquid.ChronoHelper.Internal 6 | { 7 | internal class SettingsWindow : EditorWindow 8 | { 9 | public static event Action onChronoButtonsDirty; 10 | public static event Action onChronoWarningTest; 11 | 12 | private class Styles 13 | { 14 | public GUIStyle urlLabel; 15 | } 16 | 17 | private static readonly GUILayoutOption kMidButtonMaxWidth = GUILayout.MaxWidth(120.0f); 18 | private static readonly Vector2 kWinSize = new Vector2(360.0f, 680.0f); 19 | private const string kVersionLabel = "Version: "; 20 | private const string kDialogueResetSettingsTitle = "Reset settings"; 21 | private const string kDialogueResetSettingsMessage = "Are you sure want to reset settings?"; 22 | private const string kDialogueResetSettingsYes = "Yes"; 23 | private const string kDialogueResetSettingsNo = "No"; 24 | private const float kWarningBlinkMinPeriod = 0.25f; 25 | private const float kWarningBlinkMaxPeriod = 5.0f; 26 | private const float kWarningBlinkMinDuration = 0.5f; 27 | private const float kWarningBlinkMaxDuration = 10.0f; 28 | 29 | private static SettingsWindow _window; 30 | private static Texture2D _logoTexture; 31 | private Styles _styles; 32 | private Vector2 _scrollPosition; 33 | 34 | private void OnEnable() 35 | { 36 | titleContent = new GUIContent("Settings"); 37 | InitTextureContent(); 38 | } 39 | 40 | private void OnDisable() 41 | { 42 | ReleaseTextureContent(); 43 | } 44 | 45 | private void OnDestroy() 46 | { 47 | Settings.I.Save(); 48 | } 49 | 50 | private void OnGUI() 51 | { 52 | EnsureStyles(); 53 | DrawLayout(); 54 | } 55 | 56 | private void InitTextureContent() 57 | { 58 | _logoTexture = Helper.CreateTextureFromBase64(Base64Image.Icon.Logo, "CH_Icon_Logo"); 59 | } 60 | 61 | private void ReleaseTextureContent() 62 | { 63 | DestroyImmediate(_logoTexture); 64 | } 65 | 66 | private void EnsureStyles() 67 | { 68 | if (_styles == null) 69 | { 70 | _styles = new Styles(); 71 | _styles.urlLabel = new GUIStyle(EditorStyles.label); 72 | _styles.urlLabel.fontSize = 16; 73 | } 74 | } 75 | 76 | private void DrawLayout() 77 | { 78 | using (var vertical = new EditorGUILayout.VerticalScope()) 79 | { 80 | GUILayout.FlexibleSpace(); 81 | DrawInfo(); 82 | GUILayout.Space(8.0f); 83 | using (var scroll = new EditorGUILayout.ScrollViewScope(_scrollPosition)) 84 | { 85 | _scrollPosition = scroll.scrollPosition; 86 | DrawBackgroundSettings(); 87 | GUILayout.Space(8.0f); 88 | DrawButtonsSettings(); 89 | } 90 | GUILayout.Space(4.0f); 91 | DrawResetButton(); 92 | GUILayout.FlexibleSpace(); 93 | } 94 | } 95 | 96 | private void DrawBackgroundSettings() 97 | { 98 | var settings = Settings.I; 99 | using (var group = new EditorGUILayout.VerticalScope(EditorStyles.helpBox)) 100 | { 101 | GUILayout.Label("Background", EditorStyles.centeredGreyMiniLabel); 102 | settings.normalBackColor = EditorGUILayout.ColorField("Normal color", settings.normalBackColor); 103 | settings.warningBackColor = EditorGUILayout.ColorField("Warning color", settings.warningBackColor); 104 | settings.warningBlinkPeriod = EditorGUILayout.Slider("Warning blink period", settings.warningBlinkPeriod, kWarningBlinkMinPeriod, kWarningBlinkMaxPeriod); 105 | settings.warningBlinkDuration = EditorGUILayout.Slider("Warning blink duration", settings.warningBlinkDuration, kWarningBlinkMinDuration, kWarningBlinkMaxDuration); 106 | settings.warningMode = (WarningMode)EditorGUILayout.EnumPopup("Warning mode", settings.warningMode); 107 | 108 | GUILayout.Space(2.0f); 109 | using (var center = new EditorGUILayout.HorizontalScope()) 110 | { 111 | GUILayout.FlexibleSpace(); 112 | if (GUILayout.Button("Test warning", EditorStyles.miniButton, kMidButtonMaxWidth)) 113 | { 114 | onChronoWarningTest?.Invoke(); 115 | } 116 | GUILayout.FlexibleSpace(); 117 | } 118 | } 119 | } 120 | 121 | private void DrawButtonsSettings() 122 | { 123 | var settings = Settings.I; 124 | using (var group = new EditorGUILayout.VerticalScope(EditorStyles.helpBox)) 125 | { 126 | GUILayout.Label("Buttons", EditorStyles.centeredGreyMiniLabel); 127 | using (var check = new EditorGUI.ChangeCheckScope()) 128 | { 129 | settings.showResetButton = EditorGUILayout.Toggle("Show reset button", settings.showResetButton); 130 | settings.layout = (Layout)EditorGUILayout.EnumPopup("Window layout", settings.layout); 131 | settings.blockOrder = (BlockOrder)EditorGUILayout.EnumPopup("Button block order", settings.blockOrder); 132 | settings.buttonWidth = (ButtonWidth)EditorGUILayout.EnumPopup("Button width", settings.buttonWidth); 133 | settings.buttonFormat = (Format)EditorGUILayout.EnumPopup("Button format", settings.buttonFormat); 134 | EditorGUILayout.PropertyField(settings.chronoPointsListProp); 135 | 136 | if (check.changed) 137 | { 138 | if (null != onChronoButtonsDirty) 139 | onChronoButtonsDirty.Invoke(); 140 | } 141 | } 142 | } 143 | } 144 | 145 | private void DrawResetButton() 146 | { 147 | using (var center = new EditorGUILayout.HorizontalScope()) 148 | { 149 | GUILayout.FlexibleSpace(); 150 | if (GUILayout.Button("Reset settings", EditorStyles.miniButton, kMidButtonMaxWidth)) 151 | { 152 | if (EditorUtility.DisplayDialog(kDialogueResetSettingsTitle, kDialogueResetSettingsMessage, kDialogueResetSettingsYes, kDialogueResetSettingsNo)) 153 | { 154 | Settings.I.ResetToDefault(); 155 | if (null != onChronoButtonsDirty) 156 | onChronoButtonsDirty.Invoke(); 157 | } 158 | } 159 | GUILayout.FlexibleSpace(); 160 | } 161 | } 162 | 163 | private void DrawInfo() 164 | { 165 | GUILayout.Space(16.0f); 166 | using (new GUILayout.HorizontalScope()) 167 | { 168 | GUILayout.FlexibleSpace(); 169 | using (new GUILayout.VerticalScope()) 170 | { 171 | GUILayout.FlexibleSpace(); 172 | GUILayout.Box(_logoTexture, GUIStyle.none); 173 | GUILayout.FlexibleSpace(); 174 | } 175 | GUILayout.Space(8.0f); 176 | using (new EditorGUILayout.VerticalScope()) 177 | { 178 | GUILayout.FlexibleSpace(); 179 | DrawLinkButton("GitHub", Consts.URL.Github); 180 | DrawLinkButton("Home page", Consts.URL.Homepage); 181 | DrawLinkButton("dotsquid.com", Consts.URL.DotsquidDotCom); 182 | GUILayout.FlexibleSpace(); 183 | } 184 | GUILayout.FlexibleSpace(); 185 | } 186 | EditorGUILayout.LabelField(kVersionLabel + Helper.version, EditorStyles.centeredGreyMiniLabel); 187 | } 188 | 189 | private bool DrawLinkButton(string title, string url = null) 190 | { 191 | bool result = GUILayout.Button(title, _styles.urlLabel); 192 | var rect = GUILayoutUtility.GetLastRect(); 193 | EditorGUIUtility.AddCursorRect(rect, MouseCursor.Link); 194 | if (result && !string.IsNullOrEmpty(url)) 195 | { 196 | Application.OpenURL(url); 197 | } 198 | return result; 199 | } 200 | 201 | public static SettingsWindow Open() 202 | { 203 | _window = GetWindow(true, "Settings", true); 204 | _window.minSize = kWinSize; 205 | _window.maxSize = kWinSize; 206 | return _window; 207 | } 208 | } 209 | } 210 | -------------------------------------------------------------------------------- /Editor/Internal/PropertyDrawers/BaseListDrawer.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using UnityEditor; 3 | using UnityEditorInternal; 4 | using UnityEngine; 5 | 6 | namespace dotsquid.ChronoHelper.Internal 7 | { 8 | [CustomPropertyDrawer(typeof(CustomPropertyList), true)] 9 | internal class BaseListDrawer : PropertyDrawer 10 | { 11 | private static readonly Color kElementSeparatorColor = new Color(0.5f, 0.5f, 0.5f, 0.25f); 12 | 13 | protected readonly Dictionary _container = new Dictionary(); 14 | protected readonly HashSet _toRemove = new HashSet(); 15 | protected ReorderableList _list; 16 | protected SerializedProperty _property; 17 | protected bool _isDirty = false; 18 | protected int _prevIndex = -1; 19 | protected Texture2D _lineTexture = EditorGUIUtility.whiteTexture; 20 | 21 | protected virtual string header 22 | { 23 | get 24 | { 25 | string result = string.Empty; 26 | if (null != _property) 27 | { 28 | result = _property.displayName; 29 | } 30 | return result; 31 | } 32 | } 33 | 34 | protected virtual bool isFoldable => true; 35 | protected virtual bool hasPerItemRemoveButton => true; 36 | 37 | public override float GetPropertyHeight(SerializedProperty property, GUIContent label) 38 | { 39 | CacheList(property); 40 | return _list.GetHeight(); 41 | } 42 | 43 | public override void OnGUI(Rect position, SerializedProperty property, GUIContent label) 44 | { 45 | _property = property; 46 | CacheList(property); 47 | CheckDirty(); 48 | _list.DoList(position); 49 | RemoveEnqueued(); 50 | } 51 | 52 | protected virtual GUIContent GetElementTitle(SerializedProperty element) 53 | { 54 | return new GUIContent(element.displayName); 55 | } 56 | 57 | protected virtual void DrawListElement(Rect rect, int index, bool selected, bool focused) 58 | { 59 | const float kRemoveButtonWidth = 16.0f; 60 | bool hasPerItemRemoveButton = this.hasPerItemRemoveButton; 61 | 62 | DrawListElementSeparator(rect, index); 63 | 64 | if (isFoldable) 65 | { 66 | const float kRightShift = 10.0f; 67 | rect.x += kRightShift; 68 | rect.width -= kRightShift; 69 | } 70 | 71 | if (hasPerItemRemoveButton) 72 | { 73 | rect.width -= kRemoveButtonWidth; 74 | } 75 | 76 | using (var check = new EditorGUI.ChangeCheckScope()) 77 | { 78 | var serializedProperty = _list.serializedProperty; 79 | var element = serializedProperty.GetArrayElementAtIndex(index); 80 | bool hasCustomTitle = DrawListElementTitle(element, index, ref rect); 81 | if (hasCustomTitle) 82 | EditorGUI.PropertyField(rect, element, GUIContent.none, true); 83 | else 84 | EditorGUI.PropertyField(rect, element, true); 85 | if (hasPerItemRemoveButton && HasRemoveButton(element, index)) 86 | { 87 | var singleHeight = GUI.skin.button.lineHeight; 88 | var buttonRect = new Rect() 89 | { 90 | width = kRemoveButtonWidth, 91 | height = singleHeight, 92 | x = rect.x + rect.width, 93 | y = rect.y + Mathf.Floor((rect.height - singleHeight) * 0.5f) 94 | }; 95 | if (GUI.Button(buttonRect, ReorderableList.defaultBehaviours.iconToolbarMinus, ReorderableList.defaultBehaviours.preButton)) 96 | { 97 | EnqueueToRemove(index); 98 | } 99 | } 100 | if (check.changed) 101 | { 102 | serializedProperty.serializedObject.ApplyModifiedProperties(); 103 | _isDirty = true; 104 | OnChanged(element, index); 105 | } 106 | } 107 | } 108 | 109 | private void DrawListElementSeparator(Rect rect, int index) 110 | { 111 | if (index < _list.count - 1) 112 | { 113 | using (GUIHelper.ReplaceColor.With(kElementSeparatorColor)) 114 | { 115 | var lineRect = rect; 116 | lineRect.y += rect.height - 1.0f; 117 | lineRect.height = 1.0f; 118 | GUI.DrawTexture(lineRect, _lineTexture); 119 | } 120 | } 121 | } 122 | 123 | private void CacheList(SerializedProperty property) 124 | { 125 | if (!_container.TryGetValue(property.propertyPath, out _list)) 126 | { 127 | var listProperty = property.FindPropertyRelative(CustomPropertyList.kListFieldName); 128 | 129 | if (null == listProperty) 130 | Debug.LogErrorFormat("Property '{0}' not found!", CustomPropertyList.kListFieldName); 131 | 132 | _list = new ReorderableList(property.serializedObject, listProperty, true, true, true, true) 133 | { 134 | drawHeaderCallback = DrawListHeader, 135 | drawFooterCallback = DrawListFooter, 136 | onAddCallback = OnAdd, 137 | onRemoveCallback = OnRemove, 138 | onChangedCallback = OnChanged, 139 | onReorderCallback = OnReordered, 140 | drawElementCallback = DrawListElement, 141 | elementHeightCallback = GetElementHeight, 142 | }; 143 | _container.Add(property.propertyPath, _list); 144 | OnInit(); 145 | } 146 | _prevIndex = _list.index; 147 | } 148 | 149 | protected virtual void DrawListHeader(Rect rect) 150 | { 151 | EditorGUI.LabelField(rect, header, EditorStyles.miniBoldLabel); 152 | } 153 | 154 | protected virtual void DrawListFooter(Rect rect) 155 | { 156 | ReorderableList.defaultBehaviours.DrawFooter(rect, _list); 157 | } 158 | 159 | protected virtual bool DrawListElementTitle(SerializedProperty element, int index, ref Rect rect) 160 | { 161 | return true; 162 | } 163 | 164 | protected virtual bool HasRemoveButton(SerializedProperty element, int index) 165 | { 166 | return true; 167 | } 168 | 169 | protected virtual void OnInit() 170 | { } 171 | 172 | protected virtual void OnDirty() 173 | { } 174 | 175 | protected virtual void OnChanged(ReorderableList list) 176 | { 177 | _isDirty = true; 178 | } 179 | 180 | protected virtual void OnChanged(SerializedProperty element, int index) 181 | { } 182 | 183 | protected virtual void OnReordered(ReorderableList list) 184 | { } 185 | 186 | protected virtual float GetElementHeight(int index) 187 | { 188 | var element = _list.serializedProperty.GetArrayElementAtIndex(index); 189 | float elementHeight = EditorGUI.GetPropertyHeight(element); 190 | return elementHeight; 191 | } 192 | 193 | protected virtual void OnAdd(ReorderableList list) 194 | { 195 | int listIndex = list.index; 196 | int listCount = list.count; 197 | int index = (listIndex >= 0 && listCount > 0) 198 | ? Mathf.Clamp(listIndex + 1, 0, listCount) 199 | : listCount; 200 | var serializedProperty = _list.serializedProperty; 201 | var serializedObject = serializedProperty.serializedObject; 202 | serializedProperty.InsertArrayElementAtIndex(index); 203 | serializedObject.ApplyModifiedProperties(); 204 | serializedObject.Update(); 205 | list.index = index; 206 | } 207 | 208 | protected virtual void OnRemove(ReorderableList list) 209 | { 210 | var serializedProperty = list.serializedProperty; 211 | var serializedObject = serializedProperty.serializedObject; 212 | if (serializedProperty.propertyType == SerializedPropertyType.ObjectReference && 213 | serializedProperty.GetArrayElementAtIndex(list.index).objectReferenceValue != null) 214 | { 215 | serializedProperty.DeleteArrayElementAtIndex(list.index); 216 | } 217 | serializedProperty.DeleteArrayElementAtIndex(list.index); 218 | serializedObject.ApplyModifiedProperties(); 219 | serializedObject.Update(); 220 | list.index = Mathf.Max(0, list.index - 1); 221 | } 222 | 223 | protected virtual void OnRemove(int index) 224 | { 225 | var serializedProperty = _list.serializedProperty; 226 | var serializedObject = serializedProperty.serializedObject; 227 | if (serializedProperty.propertyType == SerializedPropertyType.ObjectReference && 228 | serializedProperty.GetArrayElementAtIndex(index).objectReferenceValue != null) 229 | { 230 | serializedProperty.DeleteArrayElementAtIndex(index); 231 | } 232 | serializedProperty.DeleteArrayElementAtIndex(index); 233 | serializedObject.ApplyModifiedProperties(); 234 | serializedObject.Update(); 235 | if (index >= _list.count) 236 | _list.index = _list.count - 1; 237 | } 238 | 239 | private void EnqueueToRemove(int index) 240 | { 241 | _toRemove.Add(index); 242 | } 243 | 244 | private void RemoveEnqueued() 245 | { 246 | foreach (var index in _toRemove) 247 | { 248 | OnRemove(index); 249 | } 250 | _toRemove.Clear(); 251 | } 252 | 253 | private void CheckDirty() 254 | { 255 | if (_isDirty) 256 | { 257 | OnDirty(); 258 | _isDirty = false; 259 | } 260 | } 261 | } 262 | } -------------------------------------------------------------------------------- /Editor/Internal/Data/Base64Image.cs: -------------------------------------------------------------------------------- 1 | namespace dotsquid.ChronoHelper.Internal 2 | { 3 | internal static class Base64Image 4 | { 5 | public static class Icon 6 | { 7 | public const string Settings_dark = "iVBORw0KGgoAAAANSUhEUgAAAAwAAAAMCAYAAABWdVznAAAAdElEQVQoz5VSwRHAIAgLPafKLHY6O0vWoi/vlMZHfXGBEAhGZuLPawBAsuKzS6ygJFylyMlt+HUocPFOkPSc5l5zkZkgma6AZC94WMKybFoCgOE6mpHuqYCDQ5tqtfWzoDOimUVvAH3GkrAeNw5fY8yZa+IFBq5HP2JJ8iYAAAAASUVORK5CYII="; 8 | public const string Settings_light = "iVBORw0KGgoAAAANSUhEUgAAAAwAAAAMCAYAAABWdVznAAAAkUlEQVQoz5VSwQ3EIAxzqi7AFmQEdmmn681CRoAtGMH3uHICLn1cJKTIsZM4Qkjin9gBwMwmMIRAAGityYinlLCNpE5cxSO+eQQv/xGo6utp77EmJFFrpUcopRwjHmMUV9DNrit9BSJyeR3XiSRPIQkzcw2uU6ezega9Q+xrMed8Ajh6rqq48w/x4Wtc95uCJN7fkVGVyp93+QAAAABJRU5ErkJggg=="; 9 | public const string Reset_dark = "iVBORw0KGgoAAAANSUhEUgAAAA0AAAAMCAYAAAC5tzfZAAAAXklEQVQoz52RwQ3AMAgDz1GGCfsPk3HcVyTUqmmoXzw4jGzZZikiDGjOyU7tBhypVQEAjTFKAKBGXe6AgOymHZCDUOk/20REvvQa+QqsAaSlI8e+hq9CH1C1pz+R6wKUNx2CpAeEkwAAAABJRU5ErkJggg=="; 10 | public const string Reset_light = "iVBORw0KGgoAAAANSUhEUgAAAA0AAAAMCAYAAAC5tzfZAAAAXklEQVQoz52RwQ3AMAgDz1G2yf6bhHncVyTUqmmoXzw4jGzZZikiDGiMwU7tBhypVQEAzTlLAKBGXe6AgOymHZCDUOk/20REvvQa+QqsAaSlI8e+hq9CH1C1pz+R6wLdSyOOjGmt7wAAAABJRU5ErkJggg=="; 11 | public const string Pause_dark = "iVBORw0KGgoAAAANSUhEUgAAAAcAAAAMCAYAAACulacQAAAAKUlEQVQY02PU0ND4zwABjNevX2dgYGBg0NTU/M/AwMDAxIAHjEoSkgQANSEFj9cbB0UAAAAASUVORK5CYII="; 12 | public const string Pause_light = "iVBORw0KGgoAAAANSUhEUgAAAAcAAAAMCAYAAACulacQAAAAKUlEQVQY02O8cuXKfwYIYNTW1mZgYGBguHr16n8GBgYGJgY8YFSSkCQA3ocHkwSoErMAAAAASUVORK5CYII="; 13 | public const string Locked_light = "iVBORw0KGgoAAAANSUhEUgAAAAwAAAAMCAYAAABWdVznAAAAbklEQVQoz52Q0Q2AMAhE70yHgdl0HDsb3QZ/rGkJjdH7g0LvHXR3fFGJjdZa/IEAICLzAkmYmavqMU7fPXaSkrjWUJ9jseGjHquEfZKI8JdDuqCqRwz/lqEm4ZdXgpn5K9IKIb49VyIJAPsKsc9d5Pwp8+BxwkcAAAAASUVORK5CYII="; 14 | public const string Unlocked_dark = "iVBORw0KGgoAAAANSUhEUgAAAAwAAAAMCAYAAABWdVznAAAAa0lEQVQoz5VRQQ7AIAgT46fkS/odv9RvdYc5o7hM1oRLFdqCkAx/kCyhqssEAKKqAcBNkByVcybJMlfnxp/0otqMwvIYHbaXAfKEtt4tAIhX4WwJQAVQ3Q3dd3Pd4ZQnzjY+AtdtSx3ltNoLI+dJPoQHb3kAAAAASUVORK5CYII="; 15 | public const string Unlocked_light = "iVBORw0KGgoAAAANSUhEUgAAAAwAAAAMCAYAAABWdVznAAAAcUlEQVQoz5WRyw3AIAxD44ph7NnadTob2YZeCoKoCJpbvn5JUEqxP5ZiwN2HCSTh7kZybABgOeci6eob3hgqSfpQvXtH0pA8NrCHAU0qskcjiV2FNZKkKy6/2uGO7NM/1FMukWYIMdeuBMDM7Jwh1roHbuItb9B5vFoAAAAASUVORK5CYII="; 16 | public const string AutoResetOn_light = "iVBORw0KGgoAAAANSUhEUgAAAAwAAAAMCAYAAABWdVznAAAAg0lEQVQoz5WQMQ7DMAwDqaJ6iCc9LnlO/ThNfggHZkgcxGkLxDcJMCnKNEmY4YVJ3gDQWgMAkJS7G0m4+yAspYwJJPU44SruM8lT5O42GCJizcxPn+9bM1MAdtOlpeV4wC+DJEjaEw5qREzXWh99+nbrFxGx9tNNEszOEpY/i+tgmGEDIxNHz8eqd30AAAAASUVORK5CYII="; 17 | public const string AutoResetOff_dark = "iVBORw0KGgoAAAANSUhEUgAAAAwAAAAMCAYAAABWdVznAAAAdElEQVQoz5WQ0Q2DMAwFXxBL2SuFcehKXuv4AWRM2oKlSHZyL4muAXpTk17WJeDuw+fy/vQEjojlmBswhCOiJfhzHgACuplhZgD9mPdeey/gDChByvCvgOrNfwMjuAbmasPdV0lrEXC3lGz0gd27pfqNb2sD55vTX0C1gsEAAAAASUVORK5CYII="; 18 | public const string AutoResetOff_light = "iVBORw0KGgoAAAANSUhEUgAAAAwAAAAMCAYAAABWdVznAAAAfElEQVQoz5VRwQ3EIAwzpy7j2WCcdjazje9DEOWiq5pXsOw4McU23tQHL+sm6L2ndjc8VpJkST+CgVXbsI1iO51MskgyyQbgmreOpi4ONd6jhySHA5aUgoSV/E+AffKjICPvgmMFSTZJJ4BzC6DF4JlSpJE5pCk9/XCs9AXp0pyoA0mhyQAAAABJRU5ErkJggg=="; 19 | public const string Logo = "iVBORw0KGgoAAAANSUhEUgAAAHAAAACACAYAAADTcu1SAAAgAElEQVR42uy9eZwtV3Xf+927qs7U83zneyVdSUjXEpJADAYZZJCQIsZAwI6TOI7fs4ND/IKN38sDY2xFeEywn4cYD9h4wAQHY0c2xgGDhAAJzQIkhOY7D3177jNW1d7r/bF3napTffoKjwHH5/O5UvfpM1Tttdf0W7+1topCTTUKUIAVECAKNVbAWogC1X8+0Ar867RSKAVWFAohCBQiCmOF0L/HWCHU7nWJEQKt0EqRGotSCq3BGHGfrRQWQSwoBBRYKwAopbBWEBH3nVYQAa3AimD982IFY93PUaAuD5T6ts1u8ssAWmsU7lr8f1AK3B25H5Ryv2XfqRQg2d9AxP+N/hv9a93flP+o7HUAIjLwGv+ke5119579TfIX9F8rIv558T/3PxlrBc0/kIf4xQu00qFW7+rE6UMa9Ut7JkYerkfB66y1GCv8Q3v8gxFgoBWBUt9hrX28G6e3iKCMCNVQH9oxVv+T+dHarZVQX5ZaixX5RwF+o2id1ppAqRfY1H6yG6cfToy9oCiexAipsYxVwtfsGat+eaZeeR9KTaTG8g9Bjt+UAhTvOAKt5sWa9/eS5O5ukl43TLMEwQKxsYAwV4/evn+08sRkPfyBVCD+Jjer+ptNcAKEoSJQ9h29OH6y3Uu+/1y+rS9TBUagZyyVQM3tbkS/sn8sum+0ElwfWyG1UoxL/lGAf9sPI6C0ItLq9crKw704+bk4tWPnek+zl9BODJo8khZvVnvGMhqq5x0YrfzPvWPRR6JAXdQ1ghW+qQSpvxkEZwRqobqsHnCrWPvHcWoOZWnAdo+dozW+9/n7ADi23sJYQWeiUU6QPSOk1jJbDd584UT1sR2N8BaBWtdY5B8F+Nd/ZDlpbIUoUBMTkfr5Ec2XxchrjBW0zyf1NrryvVfu5SNvupLffNNl3PZ/PI/XXbqD05sdDq910IMpHBbopBYN7B2J3nXxZOXJmXr4PbEVeuYbX4zqGy2RtxYSYwlDzXigfmA04CdSa2fbKSTWkvhk3ViLsZbE2P7N/JODc7z9xQd4/s4JnlrpMFFTHNw3DsDHHjrFe29/hgdOrjFRidgxWsGKoBVorVC4a6uHilBr1hL7+ROt5F0bsb0j1Iqqv6dvtET+G0qAHR/aT9eCG2Yr+pbQ2uc1E0NPILGQWGfyUisYY+kkKQCXzY/x7m87n9deNM9mbHhipUU3hvOmq0w2NEopJmYapO2EX/ziUf7L545wcrPD7tEa49UAQTlBKoUCQq0YiQIscLqT/s6pjvnxTmoPVzSEHsX5RwF6AVoj9KwLHiaqwfN31IL3Tob6+l5q2IgNibW4cB9SK6QCK50YrGGqHvFjL7+At129jzAKeOz0Jq3YYkToJJb9k1UmG4HTWhEm6yHV6TrHT27yM3cc5gP3HKdjDBdNjxAFziBrvEYqqASaeqDpGrGnu+kvnGylPxEbu1EPArSm7yf/txSgVgqrhGbPMloJ5vaNRjfPVfS/FSu0U2cqe6khtpbUQoqiFRtW2l1A8e9fvI//95rz2Tk/SnelzelmTGKhm1oSI3SNYfd4lcm6EyDKwZpihfmJKoxU+OLjS/zUZ57h1kfPMB5F7J+qof2CaqWcr0URaUUt1Gwm9uyxVvKe0+30VwHqof7fT4CBUsTG0k6FaqTY04jevrcR/XhVqfFmYugZi4iQihNgYtzPT612AcurL1ngPdeez/MvnIWNHmfWewRakVihm1o6qRCnlp4Rdo5FjNcDUitOtVS+fjpQzEzXAfjI/Sf5L7cf5t4T6+wbqzE3EjmUR7kNlwVWjUgTac1Sz9x3uBm/c6VjPhVqRS1UefrxD1mAoVZ0jVvovWOV1144Uf3J6Sg41IwN7dRicAGK9SYvtZZja12WujFX7Z7kna84nzdesRNSy9Jyp79oyl9rN7X+8w29VNgxGjFWC0iRwXjblyGsFWoVzfjsCJ31Lr/8hWP86heOcnK9y8HZERqVYCBUz6Lf0cg9f7yVfOSZZvyj7cQ+WQ0UkVJOy/+hCVBraPYsXSvsHIkuu3C8dsuekei1qbE0vZYZ6zTN+hvZ6KZ87WyTPRM13vbyffzgS/YTNiqsL7boxS45R/yi+H+xcQLsGUsvEeZGIkaqGkMm6Vx4xZzFijDZiKhM1Xjm6Abv++wR/tv9J7ECF8w2CHzeqFEoH7FWA8VYpGkZkWc24586vBHfbKz0GhWNktw/flML0IqDpzYSQy3Q45fNjLzn4onqD2mBdR+gWJygU58891LL02fbhKHiLS/cwX94+QF27xyld7bDWjslUMolbwVoRcRpoRGhmwpx6nzgVC1kpBpgpGBCtRpMOAuJPQhzEzWohXz+q2f5+duP8OnHVpish+wcr3rZK+/L3RrUtKIeaVZie/yJte67T7SSDwKMhNpd3jerAEVgtZsSaMWhmcZbL5tu3DxZCWbXugmd1EWWxjqhGX9hx1e6NHsp3/4tM7z12r1cfckMbCQsrvZQXkhYr3FW8p/BF4IhtpZeCu3EMlkLaBQ1kIIAC8Ir/mzFWY3pmToY4cP3nuRXP3ecR05ssnuyxmQjzDLBvhC1UoxVA0IFR1vJ5x5b7b1zrZN8vhJqQr85vmkEKCIkIjS7hvnRyhUv2z3xG+eNVp/fSg1rvRQj5Lmc19C1dsLhs10u2d3gh199HtddOQdGWD7bxRpQRY2zJSEWtFB7LeykQjcRxqqaWlVjh5lQtVULy2a1UQkYnanT2Yh5/+eO8YufOUInsVwwV6cSBIjkkbVSikoAY5WQ2FgeW+v9xldXOm8TY+NaJSBQ9IGAb0gBhqEiSWG9kzBSD0evXhh7z1Wzo++oaMVSJ3Y5nBUnQGMRXK72zGKHiZGQN79kB9/78l2MTddYX+zQiy1a/PY1/p/IVgEWtdALMzGWbirUIk0tUljFcAGWhLbld//xUyMR0XiVLz+5yi/ddoSPf2WJkWrA3skaSnvrrLxpBaqBph5pznbN0UeW2z96cqP3e2jVTzu+oQQoKMQK62mKQnPl3Oj3v3TX+M0L9cr8SidhM0mxkglOsDh/d3S5Sy8RXnXVDP/mlbs5eME4stZjpZk4DkyaCc0LrCzAIVrYLzl5HDPSiihUSCGF6IOhaqv5HBSgGigcK2B2pgZa8ecPnuFXbz/OfYfX2DVRZW6sisWB5YHn1ihgvBKiteLwRu+OR1ba71xtJV9QgaIeqH5O+r9MgIEX4ErHAJZL5kZf85JdEz96aKrxgs3YsNRJsWJJRZzgRBCEs+sJi+s9rrhwnH/5ip289IpZSCyrKz3E1+QU7rttWhBUWtI6m0efZVOKj4CVUgRBFrxso4FDfaHaala1SzuqkWZipk7cSvjgnSf57c8d58Rqj/Pm6oxUQyQD2/3bwyBgvBIQW8vjq93fenSl/TPtbvJ4WAmI/G38vQsQXJAQxym7JhtX3XjezHufOztyg7HCUiehZ3weZ6xPDYRWx3BsqceuuQpvetkO3vCSeVRds3G2R5I4ZAYZFIoxXnBZ2SAdZkaHayGSUS4KwtElAepzaF7ZnBbySGuF8XpIbbrGiRNNfv2zx/nYfWdILeybrlEJVF9rA6URBbVAMVkNWI8Njyx3/vMjK+2fkDhtRpWQMHBr/XcuwChQ9IzQasU0RqrTN543/ROv2Dv9trpWHG/GdI1xLssKsQ9SktRydLFLraK5/sUzvOnbF5heqNNd7NHqGhcUFgVHLhQxYE1BWKYksHOZUluQh/46BajU8MBmiH/MNGZ+ogqNkPu/tsJv3H6CTz+6wkQtYNdkzdEgM0qjckHWaCWgEWlOt5MzDy42331ktfMbKKhGwd+dAMUXVpudGALNy/ZO/YfXXjD3ngNjtclTrZi1XoIRV+4RHwnGVlhcjml2DC+8YpI3vnKeCy8ewzZTVtfifiLuBCC5s5Gi3/NaWPaFxeClLEBbQEFsSYCZCdVDBKi2SS/KmjiMWKVgZroGAn96/yK/fccJHjnRZOdE1acduV8MfOoxXgmJAs1T65277z+9+c7lZvczaE010gOC+hsLsBpqVloO/b9s19RNb7xw/ievmh+7fLOXcKad9OGv2DihKSWsbaacWYk5eF6DV79ijhc/bxqMsLoc93flgKD6micFDQRlBWvApD6VKAtRSqY0M6/+88X6ys+zCbBY5d1O67Z7rmBWa5FmfKZGez3m9+86xYfvOs2ZjZgDM3XqlaB/76FDywmVYqYeERvLV5Zaf3D/6Y139+LkaR0GhFr1tfyvJUBQbHRTJE1ZmGxc+h0XL7z3hgMzrzdWON6MSbx/Mz4hj0Vody1nzvaYmo749mtmuO6aaaLxkOZiTK9XYhFLgaVUNodZ/G5AeS0UUxB60ZQ+W0DD1ylAfY50gnNr4YAgRZhohFQnqhw53uS3PnuCjz+4hFawe7rmv2qw/tgINZP1iKVuau8/tXHLw2c2bgGbBGGE4q8owOwmW+0YFQZj33Vo17vfcvHCj8zVIp5ea7ORGJRSJMaVfERBNzacWozRoeIFL5jkVdfOMLOnRrwS0+rYvp+ztqRxDBFiMTAxXgtTMKaAxNht8sKyAM05NHCYX9TbCO/ZWE5qq1lFhLnJKlQD7n54hQ9+7iR3PbHOWD1kx0TFf5UvWflLGqtFjISaw+vdI3efXHv30bXW7wEEQfDsAtRAKtDuJgC84oK57/vuQ7tuvmp+bOFks8diO+4XVI0IiXGF0+WVlG5ieM6hUa65ZoqDh8aQlmF9Pc0L15L7OfFCEFsKXIomNEsbjPgk3plSKeaAdogWMijETGtVsI0Ay6lF8PWZy2Kiv0XIUixbOYRqeqoGieV/3H+WP7rnDF851mJhPGJ6JELEpWNhRu1QiulqCErx6HLz9ruOr75rrRPfmVVDhgqwVgnZbMcAPGd+7BXfd8We9163f+aFndhwvNkl9ehJYgQX2Qsb6ynr6wl7Lmjw0m+f4tLnjoEV1pcTxBTuXbE1WBkmSArmMxNgIt4Xusq9MQVfuB06k/lQBPF5pAoK1xKUTCgFMxr8FbROP7smFv1jvRIwNl2ls5HwR/ec4SN3nuHMesye6SqNagAWAu2ok4iiqhXT9Yi2sdx/av23vnh85d3GysksIOoLMPuSqZHK+W+9cu8tb7p4x3c2As3Tax1iaxAUxkJsDBZody2LZ2MmZyOef80Ez3vxBOGIZvNsgklLAUqR+iVD/J9yf7Om9B7xmpcMmtK+LzxXWiH5RpDEa2AxoswENZBCDNHK7bROFa7zr8jps1YYq4fUJyucONHmw3ee5hMPLJFYYe90rR+hap9vK4FGJWSmHnGy2W3feXzlPz2yuPnTxVqj0lqNv+XQzne87ap977xgvB48tdZhtZcgKGxW7hGIE8PqUoqEcPHVo1z98gkmdlRoL6f0uiZPxNkmr2OYQGUgb+sHKpkQE2/Xvc8T7w8xhc8Zllb4RZbEC19vI8CBaq3aXoDqr+ATy6a19HNWYpodq6DrAV96fJ0Pfe40dz62xlg9ZGGiglbK1x99gKw007WQWqh5dLn12G2Hl370bKv35yLSVr/9pss/+S/377ju4bUWi60eSikXXVqw4gS4tmrodVL2Hhrh8pePsfuiOknL0NowOWRY9EFlLSzlZwOvPZd/NF6IZlALbdmUFn1hUdF7BQFSWJFgSBK/nQBVSdAyHPQe+tyztAloYGa6Clb4nw8u84d3nuGxky12TlaZHIk8GTlnAhhrmWtUqYUBT6w1f+n3Hjr2g+GlcyM7AywBglaKtrFESqO10G1amuuWqb0Rl1wzxQVXjQCwfjZx6hsMueii4Io3V/y7Lr/Gaa8CCMX7PC8Ynf9fRKGVOHC6/FmqtMjn7GlSWxddzmEy1TnM6BCN7N+2KvVnlBA7ARaXu1QjzateOMdLLp7kY/cs8j/uOcvTZ9rsnqlRDTS91BU7Z2oRY5WAyVpI19bmAMLlTrJOHfaNRExVA463Eha7Kc0zCfURzZWvnuDCF41SHwvYXE4xieuAVcN47UoGhSilm1VDNNKWhe8wJ2VBlIJAwBZoX9p/zRYguvBFqhwYDZfh0OefTXhDfs6C2IIxQRf2iPK1wGEC1VqRGGHxTIexesi/un4PL7t0iv9+1xlu+/IqQajYPVVloVFhpl5BKWgnho1e2gQIs0/sGGEk1DxnosaIiWleVeGib2swvatKu21ZO5OgdMGfyJAwWpRnGLGVv15e0KAk4GIuiEIFkpvSYFDAWgmmrIWQC5pzCEieRUPl2X2e2k6I/r22pIlFnGCIW+wLstk1NDtt9s1WecfrDvDS50xy6z1LjEjAVCOia4znAuRfHBYvrJu6ks9ML2D/v5oiurTCxic2kJ6gxvS5C6BSEKIerDxv2d12iHZmAYnOV0QZQaxHgbMV8hqoBYwqlO2yPM6qPED6ejROnUOQ28UyhZ75fhGlEO1qKfwubOHE9LVTSkqvQVLF2aWYyoTmRdfNsdYyfP7uFSqR6tdgi9YvVIUuIOujwTQ1qKahElbg2lHCr/VQx2O3uxp6+A2qYQ68gHAUo049JNgpJtI+OFGa3N/p3NyKyhdxaEJdMtnbykk9ixZKKVVQ+eIPEHrV1iBHlV+jBy9VpD8/IQ/ejKAqmnBao0Y1STthvZX6FNo1qmpPrMr4qqEVPLqSBXOO01KLBY3BjAYkz2+g90WEj/UIziZIVSNV5UJ/gZExTTQekqwbWpsmj/q2RGbFK2YAuB4IRrLnAoWyXgsHfKFCaUFLAf6TbXytDPl92N/OYd+2MBHV8J+HbuQiGpWZ2cJ+BA9YBIpwOkCPKSRQEDvBiEghtXXNP8Xv1BZILZ5IK/3/Z2uqjEWsJZ2v0LtmjPiqEVRVoTZSqlXF5P4qK2dSPvUHiyydiZncX6VW1/0RIQPmdgC6UoMQVlBGRko4pi68Rg/k68P/MQiSC0PQHvvsZpOS2dw2As3AczWkCLwNvqoSlyYFE5pod0Aw4wg20sv9eKZU9JXMsxu8EngNtDmp1roXUpC8Ei/IQNE7UCPeVWH0qR7xiR73fvQs93xmnWNHOtz7hTVect00L33lFFP7arROx6SJRRUxvLKWaDXoL8v5ovYBTVqIEpCByvtgZCulQMVrLs4cK7yvzcx13+/KtgU/4Rz0i0L1X22XJw4UDz1MmApqRKOnAnTDb7JYBsy3QvX5RBkSaSwulcqWz5CzxaxkHE0p9MINtssGxmArGnPJBEePptz9G0ustRJ2nF8l7lk++jun+YVbDvOlO9YYma8wsbOSq/ww81VESIpJdvlfuQRFuYJfqmyY4mbINU5kSAHYFj7bPouZHQJyi2SUiGfBTi3QEQgValeI3huiRhWSeNSoGH8V/LfxjL5CnbsPRoXWC876F0jmV2T7uCCyhlSnaBTfenCClRl4er1Dp2qZ31PhxNEuv/q+I1x15xo3vn6evZePIKsp68uJC4Nlm2S6mOFmUIUpFWsl74XYwliTISmLLamG9ZqYvSdQ+Xv0Ngl7phh6+5gnu7xgGEAgOJ8WKdR8gJrSUMn83DbggKg+3pkJzXEAB3PJUDyDK+tJyCjxg25Bbdl8Gug2U5rdlIXRBhPVkJPNHoc3OlTHNZVRzb13rvGVBze55roZbnjNLJPnNeie7NFpGXRY8HHD/IeU62u5kMQUBHiOHK98swNCVB6Oyxpf+pSKbCOogaj4nBmHr5ig/OYoRto9/87pAD2noeYF15GtsI2v2BfXIpOHIBirCLQfnZL5wEzjbObz+htS8vtRg147+622u0K3ZTlzuMPo7oi9E1WmaiFHNro8vd5hbD6CRLj1o2e4+85VXvXqOa5/5Sy16Qqbi12nHJpB8zYs8S7W/4wM1hLL+aXNd5gULIkMBDCFXLWvfTI8Yw/yt/Qju+3inmL+l2nXmEbPBzChXLjflcFom21wV8kDdiOudOSiWKdGuQZ6H5gWhsclvvIbeNXN8pjiRo5tyuwbpwl3VTj+20usPNBCTQWM7ahwyewIs40KT6y0OWF7zO2usLGR8iu/fITbbl/mTW/cwfNeNg3WsnHW1XxU4DeLlEpGdlCQklXnnwVh0SK0jcUaqGnl5FBoYkH8ImSBTaaNvnlGEEgVMyMRXWtpJmmfArFlzQtPWh+g6BGN2q2duYRccOcCDAppSxlPztII4/sW+0GMi0KdvqXiyEmJEW9ZJDdb5axABIth9sWTHPr1A5z/H3dSnQtZ+2qb5eUeY9WAKxdGuWp+jNEoQNUVC7urPPZ4k5tveYL/7yef4vjhLuPn1xmbiJzf1VvRGilpYN9c2dIq2K00jcQIndQ1k8b+87SUqBwDgU3GRxUmKgFzsw2eWuxgUmF+bsR1FRvZgpVmCy5dh+Hq3QH64hC1oH0tToZbFrbJT88B37nJU3kUGhrvJF24Kp4e6H4vVlyKsFH/A43Qo4cONPvftMDsTZOc+N0lTv7RMotfbVHbX2XXeJWZWsTT6x0eX20zNh0Sovn0p5e4/751bnzNPK//pwtMHGzQO96j00ydmRgg7To7L37M5LZlqoKfVDiQuJdalAYjimqgqQY+IFQFc+oDGxGhFmjG50ZIOgk/92dP8It3HOHCuRF+5IbzufEFC6Asq6vdvNqgQMWuThnMBgS7NGpCuaiyWSpFma+v8Ju1y2UkqSyV0H1tzLU5zNq7EuteaG3W9jV89JTyf1EeSLW4iRFNOoT1kPO+fzdTN0xy7AOLnPrEKpu6y9i+GhdONZitRzy52uHIZpe5XVVs1/Kh3z7O5z+3whv+2Q5e+co5qpMRzeNdUl/zy3gwSlyJSYq1QMpmVvqvVdoN8mn7UkwYKN9EqmkEilDnvl68wOdnG6AVH73nBD952zM8eHKN2WrEPYfX+Ge/+iBvvm8HP3TjeXzLoUloxSwvxagU9IRGLwToCVd2kPYQcynDIs2t5l+KFRq/fzMekvjeC3xQkyfyfvZK1lWbFOCeDFAwhUhRbUGbXGNHagyxMjT217nk5gPM3jDJ0x84zdIXN4lmIkYXIi6fH2XXWIWn1jqctjE791dZPhvzcz/1FLf95RJveuNOrrxqElqW9RNx32QWBTmYKsi2SExqXZVFaaEiYDQYMYi46RORdvc7N1ZBj9e55+klbr79aT7+tTOA5rwp1507qxyt5MP3nOLTjyzzb67dy/dcs5PZySrJDOgFx/mUlmy1CKpUMtsWOPcrWdLSPENwLs4qx2LrBzHWQ2cZfOZMqGTAf/+DtY98XF1L+gGBInPsTi+VQGx6SKCY+9YpJr91nKN/eJajv3uG5YdbVPZWmZ6MmK5FnGj2eHK1Q3dM2DlW48EvbXDfg2u85Fun+c6bdnHhxROwGLOy2HO4oClp27BdbPOIMbWuX15r1wYXWkXk2oKIU2GuEbGwc4Tmeof/dOsj/Oznn3b8oEaNeqhJbN/hE2jFhfMNljZjfvzPHufwiQ7/9X2XMTqraS8lSK+ALJUBAbUNU0GVhFeqo2YZTWodAzAjNBnrtLKvgYmPQh3OlucdAz7QmxpBZTlm36Rq1EANTKHAQFt3CFTA+W/eycyNUxz7zTMc/+9nOXMyZuS8GrvHa8zUIw6vdXlyzflHZYVPfXaZO+5Z4dXXzvPdr9zF9K4ROsc6bLRSgmLNr2w+swjdQuAnR3RS1ziTBm5cSGzBWM2huVG0gt/8wmFu/uyTHNtw40smGxUUbk3CAmezFac0gfNnR7i2McfsSI1mkjLTifLKSSYYU7jGcupxLkEOq5sWYhRnXq1PGSUPYlKvnsbT1GwBrgq8n8vyP424pk41iBI50MRpYdAHTxSiLG3aRGMVLnz7fmZvmuTw+09z8pOrbFY143urHJyqM9uIeGK1w+H1NnMLFbpdw/v/6Ci3fv4M//oVu/lXL9xFfaHG6qk2ibX5nLSS9lkrTFRCaqMVGisxE9WQZmJQxnVX7RmvMd+IuPWJJX7+7mf44ok1d59KEQaaOLVEoRtUYAXaqZCmlh2jFb5lbpzzJhu02saxBnwvYr/OF2RNL6UAS86hhduyBiTHoyXfo0Zc+5oUobRsqm0Re8uuKi98Z5iMQon0CalFH4gXos6EnbU+K0VCQo+EsYvGeO77xpj/9DJP/8Ypzt7fpLKrwth0hcvnRlloRDy12uFw0mbvzhqLaz3+/Qe+yoc+d5IfeuUBXnX5ArRTFle6/YYRxA9LF1iYHSVtJ/zoXzzGp55a4Q0XLXDd+bM045TxasQTy21+7Pbj3Pr44qCvEUH5Wo32jPONXspMPeKKXRNcODXKSDV0mhinNMRPo/Bsuqxq4oavb+P7zsHWExmOpVpPLstmomqlfGSaCdDPH+unCSK5ujqD6YWoBkp2YsXVrUqVkyyyc77RaSsFLm2XLgrNrlfMMfeKSQ7/zmm+9iunOPnwJiMHqkzXQ8ZrY8yOVHjo9DoqggsW6tz15Cr/5PEl3nLlTn7m1Rexd88EncUWG50EZWF+pArjdT714Al++C8e4ytnNgG458Qadxxb5Xuv3MMfPnqaX7v/aN+flB/GCihLLzGgNZfPj3HF/ChT9QrdxLLa7qG17scKRZ/cJx/oIfVJOQe9oxi8lDRWeXdmbA5vqkIu4ASYce79uxw2nEE1MlBQHxSO32k6vyLtgx3rvyJL+DPh+8FUWIRN2lQJuey797LnJeN8+RdO8MRfrLBa04zurrB7tMro3mkePL3Ow2c3mR4NmSLkww+e5NNPrPDj1x/krS/aQ328CqI5u9jilk98iV+86/CWtfrEk4t86umzbmLTs9BhUmOZqle4cscEB6caGAvL7R6Bm82NiMX6lEuMDDbdsI2v2yZlyKIUscMwOZVbRK9xyiNlSkpgdkapVAV/KFZK8FzucbN+cJc3qj5dwPlM8XqbC85YQWlFOJDLKsDQps3MwVGu++WLuPCTS9zz/lM8c88GlemQ8YUKV+6cYKZR5Uun13lytcn8WI3VdsoPfOxhfvf+E4W6RmwAACAASURBVPz0DRdxfLXL//Vnj7Ls2wOGPdKvYzZ2qBWH5sa5ZG6UaqBZ6cQEWhNqd24FgaMydBPL4kqMJOKgLONjhDJXVc7BmaXUBseQEpYUsNCiXAtNLqHIYLXGGJeYW5sbZ6WV4xUVNMt4bXP+R/WLolmCnxaEbj3uWJyxEwChf33LdKkGisuun+PS68d56A/Octuvn+axhzYZ3R0xORFxzf4ZdoxV+dLiJs3YUo2qfPHIKi//tbv523jsm2hwaG6MuUaFZpzSig2hVi7qQ4NWBAinlmJaLcMbb1pgbCQkbvvinBLEqJxgZLbxgYV1FVNKHYZ0a2UdSdbX+EQ5+2fzRD6D0PI5ZanHA3OOnPRPPdmawDuNRVSf8qIQX4lRnhXo7XiQm9Gg0EkUAdpYNmnTCDQv+ee7uOLVU3zyl0/xZx84xdFjHRb2Vdk/2WBupMrXlpp8abH59VGgn+UxUQ05OD3C3okGAGfbPQ9a5wPsBFhvpSwtx1x+0Rjf+x17ecHVU7TXEzcORRdgP+V370DnsBokdmUDHLbUDmVrTuvdm+03xma/FxJ5YzIozWf8hdDYpp4PEnhKn8qCGfEbzKcNVlBBbrfDAuzWB2ENiHa8ToeqCAFCCFSUA4J7PYPoFqPjFd7wzvN43o1T/OHPH+dTf3oGVVXs3lXj8vkx9o3XefDMBl85s/7XElykNbvGquwdr1MJNMudmGrgzKXxk3wDBe1YOHW2w57ZKm/9zv285aYdVGcqNE91SRNBR0OKxzZvl7P+PlVhiIPNOqtUAdoa0vyTpTKmRIFVUObE5EC2WHEbyF+EpDlfVwdOkFpt5aRn82LQaoC/ZAsJCAVtLQovQgj8d4dKqKCIWzGm22PflSO84wMX89KPTPP7v3aMe+5bY3a+woH5Otfun2H/RJ37T65xqtn9uoU3XY9YaFQZrQS0E0M3NUSBJvDhWajdTJsjZzpMNiL+xQ27ePP1O9l3yShmNWb1mbaj92k1SGIuJeMZrim4TmNUJjwZTOLtEO0rdDRZD3VmSE9R3qH1qENic1sbW4sY2ULayVyd0oPJZF/DJBNivlsir6XpAD3Feh/oheefDxAiXyk3idulrcNdAqN40evneMHVk3zkd07wsT8+xYmVLvV6wI6RCtdfMM9jS02+srhOK9ke8netWhVGI40Voen9XKgztrcQYjmxktCNLTc8b47vu2kPh547CV3D6jMtN60wq+gryRelXIAeBu+pQl8jgxH8Fn6OKgmxbxKdOytooJD4om5GrzDlZDOjPaQgoS+jWIcP5kbS7QsjLhqzBZ5SmJdRB2LQ0P8TTzAKPItBUt+cadwrTc+y8WibRhTwnW8/n+cdGuOHf+xrtDuG2JeLDk41mB+p8Nhyk8dXmgNJcSXQTNQqjFRDVB9eg0jRv9cQWG7FrG2kvPiiKb7vxj1c/+J5ULB6rI0EoAKVdwkX6CBb+kBKzawDYzEzm6gKbLgy84C8dmd9ZmALM3CUKvjArFRhbI655TwSydU98ClEmtPAs2PinKD6MW6f8GVEObPbTy8g8Xe5RXj+OWXdVApsnybn/CWKzqZh7HCH6cmIeiNgYyMhrATEqaUVp0Q64NK5MabrFZ5cbnK249KK6YYbvhOn1g81z3pIXTrUjg2PLbZ4ztwo73nzQb7nut1EkyHNs116qaBDB9KXczkp9x5KIYDZQshRg0LS20Frg9rolsB6v+ytnufvujTCD1o1BcGlYn0eWCLAesqVTV3bsi5cl9IQqhxuy9BT6yufOhOYF2SofFXf7/4g6ylIc1RCvL8QU6hnWWhuGpLU9EecZLWyTmpY7TizeGh+jAOTDQ6vd7nj2CqBhtl65M2P22thoDm10SVJhbe+dB8/fMMB9l40TrLZZflEuz+AYOuCF5siSr7MbEN9LNIdyxUKsw3JWPJ9bK0gSrYwwUMjQmwsVuXs39SSO81+LO0v2EejYpWzBIWGpCDCzSYrwD5Zc0p/0E3R/Fu8YJ32YcGkeSlGrGw71CDzC5nFSIwbYVkJHWLSSQ27x2q8ZM8U503Wuf3oCs+stRmvRkxUQ6wIp5sxB2dHeN/rnsM1z52HNGHp2CZhRaMDNZw7WKjfiagcPhvSiSxluqIpWDStSj5zsMLSb0Wz+SZF8pE2WfIf2n65Iqv+OlKTLZivvq22XpBhznjuz4hWgskqxgVWlfV0OKs8IJD92ZvOMIPgiuF19t2mxFYrdPQWr7eXWgKlGK2FRIGikxg2esKZVkw1DPiWuVEunGpw76l17jy+xsnNLrONCmvdmJefv5trnr+LtePrxCEEdbUF1eprnBrSOq2HNekoJC0k83oIImNyuGxoeazgwUypmVl5eC3nhfr+CvHHnBrrqXu+L11lVKlMkEblu0ZJ/2+CYFNFEG3lySbiTKbWagD81UVfXojexJYQjdIjsUInMUQVxWQtohEFGCs045Ruavuk2J6xrHUT6lHAtftnuGx+jDuPr3HvKZc/rnZiWOsO7NMtpq/cTjSsAVSG/F4kQJnSNA1TsG6qRHGUQjlJBJsVdCVnpPWzCpMRe60fDZmV7iX3Q7Y4SCDTjqylydPoMvNgU+lPnVDWpRGhEheJZuUXsla/oqnNtFL6MNPgbNA8D+p1LCfPdGmEAbvGa0zWIjcTod8i4Dxtat0RBG6WtuXYRpdaoPmnFy/w1qv2MhVUOHUm9uRbCtR22Z6XWmi71UGpt1GKEbsMGe4gA/zWQcSm9Lp+9FpAybxltOScGG2s9KcupT4fjK0g1uZf6IUoqeQDePomNrNl/iZSnwYY588khcixyglEUP59OjOdqsDLtIXgxRaQ+kwdjNBtGRb21fiu1+wm7CkWF2Nif6yqSD7NyFHSC+0CvnWgmRqOrnVQTXjbCw/wg99zHvGYxaxb1xWktiEdFZPtzJmr4a+TRCAumczCBuhXMUoTFvsKUqBWSqlPwvr5MHYgD7QuF7QF9HuA1ucjLNEggfKU9AIRNSjCQq6TKKtmGCMEoogiZ2kzDurAZKts2KkMQfEzbDEBKoquEkYaEf/POy/iZS+a4fc/coJ7vrLG2ETI6GjQ9w0ZWlTsNbUinD6bUNWKF79wktfeNM/EwRrrqzHRTIX0cIqsWJj2OFq5pmfyPkeltqkgpNl0jFL64De4snnMoSwEqsSjL/lC46FNV2vuF23zRD71AkwLxBrJTFhmGrPuIR8iS6nbSPzhjCrzjxR2qDfRgXJCpDD8O/BBrfSn8xZ4JD5lkJ5nT09qVAj0oNNMSTZSXnTtLC+4YoKP3nqa3/nj4zz89CY75qpUInfgVXYvOlCsb6Z02oarLh3nNdfNc/GVY4gxrB3rohuK8FBIeEmIeTrFPJlAx7q2cilMJygOFeqX3xmYtmhjv9lMIfUweQRqCw064iHLQHk4pBS8SJGnRMZ3lUEoLavAF+d79euBtrjrCv17Wm0pk4h2UamScqeK+9HEoEURRl7GBZb0APs6Iyf1BGUUwZSGqvePnm+pQge1rR5vMxIFvPk79/LtV8/wa394lA/++TGsgtnpiCBQJKnlyOkeB3bU+Ndv2M01L56EmmZjqYcNBFX1/JcNQc9qKi+pYC4KMQ8lTpA1hRpTW7RNfFBWDGAk8T0RpmBNMvqYyfO6Yqogpch6kP3uhxtJhoW6L9IFJQiLozozrkWStW8ZNVjTUgWbpwqf4jN6KVTfB3au3wBGIMAJUQoQky306qlEkB7oeoCa06gIbNs64XlzLX6Gf1jRtLuG1tEWs9NV3vX25/DSK6b4+f92mDu+ukylF3DB7Ag3vGSGN/+THczsq9Fejuk1E3RFuepJZl1C10lk1wQ9r9E31NAPB6QPxdgli5pQqKoaWnmXjLfq26KLkJiyLmoUK4MtcQWzm8mm3z4tflR0NrPA54EZzdOg+h3QYXZSZuJpg9mZRTZLI8yQqbcZHaAICwXS50S68r/ra1eF1i3VP1pHufayLB/0c0FVT0gDhcwHRKPOFNmmdTtRe8SgP6DVdd+oyMFcaxs9grWYl33bPC+9bIr3/cHTHD/Z43tfvYfLr5pAYsPqyS46UugoayRV+b1lQtQgmwIVIbyqQngwJH0kJX00QZZs3qxSSHsky1/jPJjLgilVCKSwwxlpA/2YAzGA8nl0xlNSfU6E7SMxVvzxbtLnfmbQDcYNjJNsPkxQ+BI9ZDdp6TceOJPq3xt6OrwXsADGKFQoKANhz2lnPBmQjGjC0JuBIkXcd+4q7fNf7RAeHShsAKoCNoHV021GKwE/8i8Pun7kKmysxq7Xo5ILKfvn7kuhosxve62MQZYNeiKg8ooa4aGI5P6Y9NEEeoKaCwqm3zewJC7qlkIpqc+Xtc/Cm7HDR7H0PVnGUSp0kfUFmFh3clgmwNQTdoqD5CTrCFGFTlVheD1MFxF25Rlsg8MBEUEn7pdWTRM3NEGo0MaierJlnqfKRmxlAxFMHs6rMPclWinaPQvNHrqiXFChQIWFcZJe4zLhEZUG4Nm8SVY6Lr1QU4rqq2sEl4akDySYEymMKFRNQwIqdoGYLfFglJyD5KS2adIpuC3HnDeuYK4ygrUaTCOMFazKS/Wp51luGaTqI1A3s8x/V3Goqi70thVNK3m7sGhFkAg6gGQkYGMkIK5AVYRK1ryZ5YZ9+EoKw+jce60uQHb+rKY++uHny5jisNei8PzMUBV4VD1kcO5byb8pLcgmSEcIzo8IDkSkjyYk98fYUwZdzyPKLb3752orU0O6lkpQWmYRHenaV/ZVMQ/0JaTU22prc5s7wOvIeP/ah8uoXBOzhdfulBQVeC0tdr8GzqfpRLCjmu5kSGtEYxRUjPhKReniKfneQreNigpC065BVEzBNGZCi0pjP/xG05kgw3zzDa0ulBlkKwYqiujyiGBfQPpATHpfTLJhURM638Dn6v8rciOGdSMXGzttTvsUH0cU2yG9CQWjchSjLzxTComDQmSZChIWriDLdQJfgQgKtTIDqiVQUaQLIXZGkwSaihW0LSBQUur3L8w9U77/PBOCkjwadabK+93yPGxdapcums4QN3ggGDIMQc7RZdQT7KkU1dBUXlpF7wngizHp46nbwKNqe8HJ8DEl2wqQbfpkBk0oWJ+UWQo+sDhc1RQ0MSh8+7BSSgZnaYVqO9tt5gLSnSFpw01fClLJR8Rk61OcSqTy0cJ9Lkg2estHo0qDBFmi6X3EwNiS8hgT8oF9gRtUrYIhvqg8IzQfFjBw/I9sWuy6oKY0tRuqpOcHxA8mmBMGVVeomtp+OpQ6B1znI9CsbOawUE+X9usy0BuRmdHs/S6NKOCcvtSjpDDiQCsn4LBkcjIaXVeQxMJMgN0fYqZd90TUs1g/EU4KNAQ72N8/OGCnOCSnqGFZ47splTaKf4/UwKIpVTCdfXoIA7PY8jnNQzTESN7Hn13kijv3NzwQEuwMSB5JSR5OkBVx2hiU4gLN4EgxVQK7de4As2qEk59P0ZB8Toxk/YEqby3rczBMoeEwS1hjX0UIC6oSFm68J0hHUGOK4OII9oSIgiCRAmM8p12UQ+ssJdvijwZmVRdmihZ8ifJn2PbzxUINT2V+UeemE83ACaDD5mYP9ujnGORAkc6D+XbZQqCILosIdgUkjySkT6YQgxph8OS0YgVDMTg2Wgbwe/9P+pU7W5gY48BsA6nO25mMtT5JVYMdM754aLM1Umqg6Jt1qIYHQsILQqhpbGpRptDvofoWb4ubCQbX3C1WoYTTN5GFoGTL8NfSZICMeKSyemaY8Tmyo+py7R6MoHOOj6Qqr7xkQrAMsLAlOxYhttACVVVUrq4Q7Nakj6aY48ZpfQbLZdivVnlpCbbMT+0rVCFKdULs54H58S5ZFJp6LmK/TEQhjDc+yfUtWIjAposKw50BwYEQPaX9IRTGN4TkLG7xMy+1zlO5fsZg87xJKI4EOcfIq2Gheemfgq0jvSjwVzLfUg4i+sGZ3YoJl0afSDw4TUqaruNLT2oqL65gjlnSp1LsiiuSqpCt517oreUnEQrUEelzekw2blK8g8yeyJgAkgnPmz5CyZPd1JdNuhZiQe8MCS+KiPa4bN00bQ7B9YcIFP0LBJFyw0qLVZe0wMWhGM3IVnNarmJnx7fZQYJs/7vLNSzLlum+2Tm8lK61X3il/P4C98XIYD0vu6Z1F+iF+zXBngrmGUP6WIpdtS7ICYcgNAWGt0vr8v5ApYquqBiFKtvv+zNZIp/V4coj/ROQpqDGNeFznNZRVaQbNm/6LM5fKWPA2qUtSuV1w6xHzh0QVRpCnR3kYbciGLLdmMni0BxKacUW1lg++EdsgVqhB/2VS+rz5/qjRmyh8lDqa8jMt910cYPerwnHIuzTBnPMuHWsM1h/tIXWMxggbykZDPrD1IK1tk/3yHiIfQGmhchS4UZoBBAeDAkPhjCm3HMtnwOGhUaOVIabN5THDCUPILIRIZQW2uRkWClpVz/3tEOoehSnHqkcgRk2r6XUo26Vj1BLDDNJCuBC9rweHBqE5xMN+NZsj7QE6TpBBhcH6AWNeSbFnHYhvqrpPBrNcGMBWxhvJiW6aVicCCsFlkSRA2ONy3mUQLAvJLw4JJjXSE+QVVugzfnIIBwCdFNCVPx3KFPKg1RW5S3xI8sdP+XzlyxDmy2dWZTSaPmSnyuaPn99YnPKoHg/NwCuw9YDs6xgkwIBinwj9kdKiitKS1dQDUV4KELPBpijKXbNOt8YqQEtLjMtsj3ZrwdayekUmeL0a1RdQTZdjSx4TkSwx510ZlZszkgz+cSKjMGds4RL6xWqQYaWkUEmWn9EcZ40S7GB0bopt5LQPxtpINApIR7ZyBE1DBmxsjW5LkSZtpgupJIXZ4s5Yx8k93ltcTqrLwDYYk9EsS170zPbZzR6IsKcNJgTxkXzdVV28VuC1D6xN/EdtCJZJ4xxSMyqu4voqojg/BAqPrqypeKut1V9KmHG8s52bva7Vmidh86SEaSySDX1Ny1uSm/fLxZzDp+LSuwXKpG8lyKVLUl3Nq13y5DXYu9eFqxkBV5TEAC5AMWfmUGgcsql8XjrwMmjpfxo2Klrkg9nkE3furczIJjQ2JMGFi108jY/U1L+nFLhwWtlc2b2mWZMTwscqhKJoOcCZNM6PzfsTIUCodJmpsezrwYrGq4akdlqSQqU/UJILb7oqyh8RsFPSub/MuGVzljKmHGkylX4+7O3S1qdBRqqcHaIGUIdsYVan+8DEcmsgEVZnc+JKQdUpjT0R29j9lOfQ4agDgREMwHYGp0kX8Air4oBKE0sSjTrvZSVdsIV8+NMbIa0Rw2ju2rEJ2LitvVJcWnyvCqRXbPFoRSZZUY29fYqzcHvfGEHMVcpY7BF9EL8VPfE+6bMNKWetpcIKgVSF8BY4xtw+lo92KcngZuEP3hmgPQ10mZnOAVqywFdItYFb0UeUdk0DwnmrC2oVrakXZieCGF/wBOPNFmo1ZiuVzi+2WW6XqEe6rzxyOESbp7YqWaHhUaF73nuLm48OEfrTMonv/8IF9w0zsU3jTO6t0LnZIKJ3RDzPCUoYKPlEoyVwW6dfrQmW3OrLOLsA+gyiBcWqQvF/CsdfJ8YyWntZjCQkiKRKEM/iowyXzjOE38fefeJzA4ukmLUWj7fPovci0MB9Fb/K5lP7XftCmONkMp4yPGjHT7xR0t86asbnDdT50defAGfObLMF46tstpNmKtF/phcCE+t98Z7XctrLpjldRfOs3+izul2j1bdYjfgwd9a5pnPNbn0dRMc/PYxAFqnkpyzlC1UIEMQDL+ohQ1dvPF+4qxydMOmBfcaDJoaKZrJ4oAAT4G0hfMG+4zxRGVjFV1cpIb4JnL4TApn7CotffyXpBCbmUKOnJaibX+dWSFY2Ry6608czu7FE6arkWJ0vkZrNebPPn6Gz9yzTKtl2DlbYdMYRqOAN128wBXzY3z68DIPn22hEzMKoL7rhXs+9R0Lc6+8es8Exzsxi+24D98IglWwvpjSbhr2vqDBlf90kl3PG4V1w+Zy4rSxeDCGLvY2lBpTAhyZScgXWylUhX5BVrpeSKHnqZQ1rcACt7GFng/Le5ILOGOQ1xWq7pht1MnPqbGeOd0rVQMioFYIZrIN2vHH4IVANasJ4tnXkk/b90cHSMpAGYzCEUEZVUVSt1ST0xUwwu0PrPLnX1ji2Kku81MVxhtBjjd4ZZmpV5itVbjz5CqfP7X6S185sfaD4WWHJt94qm3f8VSz866a1ToAen5rW3HjjevTAeG44pl7Wjx5f4tLrhvnRa+fYvxAnd6ZmG7T9nsFB8FY2UIxsFIaxeE5HqIHtakfrGR96Olgg4hNJSfQFjZM/6zB1AcZoachpAWzLCVtyKylUu4zKRx8FHtBmdzliz9d1PqjAnRFUOKm+fa5RMUzjRikUWKF6fEIqpqHH93gY3ec5YHHN5kaCdm7o4qIG1arCpWwkcAJ9IGz6197crP9ztnx6C8A1DvechFfW+qwUInOv7LWuOXyxsh32kRYt2kfx02NJbEWtKLdsSye6DG+EHL1a6Z54Y3TBKOa9gl/yIcaehrw4DFeuhDg2FLxNDN9gYJqAUA3gyd5SirQyZEi23GlrMy82lTcCTNVBRWFbqhBekXqtF1iBtIIXVO5llkXVEjPm8Qgv3bJkvpQoSP6UWnfUmwZ6CPOz9VDKlMVTh1r8dHPLfLZB1YRgV2zNQ/8S/+IcgGqWjNdj1hPktbdp9dv/uLJtZ+1Ilw8N8IDz6yi/uN3XMzSespqL2Y1Tnn+/PgrLg/r792tKy/sJJZ1k5JmY0j6oIVlfTVhbTll96UNvu0NM1z20gkQaJ6K+wyqoZOJsnocDHQ3SakpUilX4Rj0g4WmkNQvbpozuel4xMOfeh1UFLoCEqm8Ql5IxJ0AZeDcCFX1rwuKQpbhB0bqkn9LcqsyYHX8IciN6SqdtZg/+eJZ/scXzrKynrB3rka9ovOymB9JopViPHK11EdWm7/55ZXNd59udk9XUEzUQubHqtzz1IoT4OJagrWWVmyojYYkRtgj0fc/f3T05rFUz6+nKS1r+1F3akw/GDx7OqbTM1x89RjXvnqWi14wDpuGjbPJIP2w2CgZFgKU1O3mfoW7ADn0qYBFE5lFe4kzbbYneQTbtpi4YIpDCLJm/Jp2QgxySM32PJ+zSAeJQNUUOlBuI8T0T9PeUnnOesMza5LI4Bm+/mTqiZkadA233rvEn951lidOtlmYrDDZCH01S/UZZ0rBaBgQasXxTu+2+xc33vnUavuLM3V3hm6rZ6iGmh3jToBhmWM6EQQ0U8N965u/thSkHzpUbfzYLh39yIgNWDUpNqvc+/ljE/Mh1Z7mobvWeejedV507TQ3vX6e2QvqpIsxzY3UH62df5OyXjjFuV/l8+AzAq8Z0mZtco3MtFil+VnzqtB7bsSd0qIzhnRYbLocAoanConBBgVozJTmuPRpA4W/F3NR31QzPVmFqubuL6/we7ed4v4nNpgajThvR90xUvqELHdtFa0YiwKW4vSZh1ea7z7a7H4oTi0z1YhGqN2MnfJ8tyFUSAKlmAlDRGg+0G393w/15IPfUmu8dyGIXh8nlo4qjOUSF6nO7a4Sx5a//PhZ7v3iGtfdNMuNN84xubdO62SPOBtJJa6o647QHtK+VayJZeP601I+aAvlrlRQSd6IqmQrOcn6E1SCLPPNKBimdHJ2higlBSrJMCFTmkhRiJCNESYaEdFkhcOHN/ng7Sf55EMrhFpx/o4GWkGSWn/UuNO6SDnBtY1NH1jZvOXh5dZ7W4lJ5xsRtUARx3bbgWLhdkNxBKgoRQPNM93uV+/TzTec36jfNCPhT44bfbm1hq5I/+g6ETfmeM/+OitrCR/8wAnuuGOF171ugW972QwjFtZP9fr5EcbNnBmYgFcWotcSSQrVh2LBtB9tutOtMSWssQgA++tUBQZ2Nmwui0K1yqE4CgFLBhC45prMrBeiXSOkqVCLNFM7GmyudPnNPznMf7/zNCutlAOzdeoVPTCo1RhX9xyNFEorHtvs/P5ja+13bybp4RDFVCV0o6KfZcpi+GxjqQQY0ZqGBJy16cef6HU+vieo/Id9YeU91Z6ejK3pW5ZseHpjRLN/pMbx4x1+6mef5LbPLvOW1+/kOVdMwHrK6krsdmCfa8pAgt4HmQOPP2ZBhBmEnpRHNEy56Nwv/Do8NEtvxDpGd/+8JgZb/zLMVGeEqUDlRW5vmgPl2t4ycCFNLEoUc9N1sMIf33GSD372JI+daLF3usaFO9zM0djYfvEGFFWtqEWaM3F61+Fm911HNrq3VbRishrSS+25T0L7qwiwnxAoaKBZN3DY9n6hOya/Ny7BT4zH6t/pRNHG9aKnnl+TWmFmtsrYRMhd96xyz4PrvOoVs/zz1+5idv8IyekezWaKDgsoQKE9u9/9lCXl1mGb4lMM5SGuvvDKx8gxWNnO0AZlh3NlFHmrl/jeRYUDwovn1pvUHesj4lKVubEq1EPu/uoyv37bCe54dIWpkYiLd40gAr3E9nNM8VhBPVS0rZx6dLX17pPt+AMKGIuCAZj1632Ef5UXZ/4xUgpjZfkp233biNa/tbtReW/UUTfY1M8zURkI4LCyPbtrNNuGD//JSW6/a5l/9uqdvOW6nUztatA61SH2ZZqByRRFJMTjhiYdLAtJUWulZF63FJJLqUCxd7GQh2baaaU0iEdys5skwkQjpDZT48ixTX79s8f52L1nsBYuWGj0Dxkp8km0iAtEUBzpxj97tBnfvNZNWhOVkEqg6Kb2rzU4M+Sv+QhR1ESzlpgHdD25sa71ayst3lWN1QtaifSp+m4KhiWKFAf3Nziz2uOn3/8Uf3HHWf7163Zz7YvmGelZVhf9cTZmsFNX+ZqiTQtBSpaR2NJ8Mc7BBCgTosrHlYsanA1X7rryhe9qpJmYa9DZjPnlP32a0Sg6SgAACNdJREFUD37+BKfWelww26BRdQeAJGmeEiiEqlYopTjeS3/zeCv+2VjkiUjBWBgMHGTF36cAs0dVKSKrWE7TW9NQbt1Rj/5t2FQ/TkcWeuK6nvrpm7FMjIaMjUY8+nSTf/8zX+X6Fy3yf75mL5dcOglLMcsrPTcJOGtJ9p2tUoowB09w2aYib2W4tg07TlVKMFshuMrS1/mZBij4k7tO8SufOcKDRzbYM1XnOTtGHLxqTH96vyBE2h1lsGnlttO95F2n2sldygqTtRBB6Bn5my7/31yA2aOCMxsrpO+vTegPhaF6T9BWP5x0LIkftZX2jyq17JirkqSWj995ltsfXOZN1+7k3924j5ndo7SOt+nEth94KCk0SMoQrbNsfwgyJe1UavtzbnWJxOy1bna0gp6ocv/XVvgvn36Gv3hkiYlqyCU7R0G5AAXlB+L58ZnVQBPDkeOd5F2riflQiKKulat2/C0+9N/2h0VWESd2cykw7+hN6RcFo8F9oVXEPeOnr/vc0TrM7+DuBrVqwH/+6NPcdPN9fOKzpxiZqDI7X3fmtDwMp1/g3TqeeOiZSuUAp9jzUY5cC6mITYV6oJlfGKOVWH7qjx/jVb90L3/65UXOm6mxY6Lqom4j/bOnUmvRXpPXxH7gcDe9+Ewn+VAgUAv+dgX3t66Bg4JURFZopvbu2qS+OqjyA9E6P95tp3Mpbnyj+NZuK5ZKpDi0b5SjSx3e8vMP8srnzvJDrzrAS79lHpqJO+QDldMPz3n0eOHk0WJCr4awu8veR1xPeqQVs7OjEBt+/S+f4RfuOMyjZ5vsH6uzd6rm0gJMH7N0RDqhohWxVncsxuadHWO/UAkUtUCj/xrR5f9SAVKgukgqNAP5r+GM/lC1Hr0n2DBv73ZTEj86MbUQG8c+XpioMFYP+LMvneETXzrLd1+zh/94/Xkc2DNBstRhpRUTFOkXW3rOpV/cT6wfU6JKh1dKSZgDFS/L/GQdqgGf/PIZ3vuZp7njmWUaQcBF0yPu4JLUoFBo66b8auXMZarVkaZSP7aRmN9tx4aGn5qY/p2J7u/AhDK8vwRtoBvbdTUW/FBlPro8HAv/VAt0e9aNuvQDTXupm75wcLbBzEjEr33uCM/7qbv4yY99DaNhYdcYFaVdLVCGf5ex0E2FVuLMtCrhqBn7ut/Y4s3ldD1ibsc4Xzvd5F988CFe9YF7ueOZFfZONJgfr/XHUifGzRTopYbUGFDKtrW+5WQiFy31zO8GHtP8+3rov9cvSoWOsV9hInjt+K7660fGo0eS+P9v7+pioyqi8Dd3Zu/v7t1tC1vKlhYoG9OmiQSDRnkg0aAhUYOP8KI+iUYfDCExAST+YIIhaKI26YOIRAQTTSAGIUjEaJUKCBgCpGhRLG2glArd7u79mxkf7t1tsSA1JHYXPe9z77n3yzlnzjkz5wvguDzqOYYcTh6XUGMKmmssjLoCaz7vwYI3D2FHVx/spI70NCtykWPmUxp+4/gSji+vKxRPiJFRvOM8ZOxMN9jI+wJrd53G/He6sf1EP5KGhgbbgB95CD+qJ3gR1xRVAM7ojlFKs1e5XMeF9FRyc6brqgdw/BmoIORU3x1Pa+2penOVYbCR0WIA1+NjHIYA3EAiZTDUWTrOXMphxbYTeKTjB3T3DqNuZgLTbb08DtPnEkVfwC1tKrgos7GFrZqxEbhCSCgSmFEXR9zSsKXrN9z97iFsONgLl0vUWjoUQuDxqLoUWZ/jh8k2jdHDgaouyUllhRuIc2zcqf1/W9hUvLRUBnMDAWrSzfVW/CPNZK8MDBZWjjgBFBZe1hNShIV+IWEbGtxAYH/PEPb3DOGZ+5uw9sG5aGywUbicR3/eK3NeBNFFTEdIUEqgKFFaF5F71ScNQGf46swg1h/sRdf5YQCApakAorpllBYoJMxDFQKYKrsUU+l6QZXO0vxtqkwRclNlgRNili/gCzGYqtOfbW5KLMykzS+5AEaKQZmMmcvQoggktFg4D6Tz0O9o2/QtNu7pQSCB7DQLXIRWGPBw4HqRC+R8jpwnkPMETMaQTtv4eaiAJz8+joe2HkHX+WEQQqAxGjaqhQitlwv4PGQyY5QElhHbxDSW9UA6uZAThkz8JwEcv5svOhxSwdHGBvPh9rn20pk1+rGCGyDnBGUQhQxjZOnH5VyOl/b2YGFHN7Ye68dMW0dTyihTCflcIudyeEJiTspEzhNY98UZtL33Hbb9NHDd7tXnIrqHFwJX9AMQSCQM9YOEqbYQSld7XOQgZUUAN6Uu9GZuVQQS+SKHodN9LY3xfYk4e67vcuHVyzmnDhGjSmlw63g5O5TH05+dxPZ5A1i1aA4Wz67FxVEXA8MOsikLCgU6fuzDxu9/Rd+14g2L9LLcqwvTm5ShfRM31TVcki6vNGQHlSdKpSlECOD5AgU3QE1C7VjQkprXmkm+rdKwufl3WdWBX65g6YdH8cKe0ygEHAsySXx9/g8s++Q4nt97+obg/VUMlfVNs82nbEtfLEC6/HEWX4nCKlg3FFwOQ1WuzppuvJg02Zb+4eIbfVcKj95q3ftHL2Dv2SE8MCuFT09dnFwtlykyaWobtBh73eNwPc7HKF4rWJRKVo6Q0GXmij6oopxsbbQfu2dOzRO1lnrqVmsHRpxJgUcJgW2oO2sSRlZX2TqfC5cLUfHAVQWAY0AS+Fwg7wSI67FdrY3J9raMvdpS2ejtPNdSY0emJ40lpq4u97nsLRFBV5Mo1aZw0eco+gHSSX3T/NmpluY6q/Of5mJ6jA7WWPrKhKneC0IOeFxg0odQ/gfw9nNHSCDvBOBCDmZqjZXzm1L3zUjqB261NkYJkoa6OWVp8yhVOl0ejhQjqF5RqlVxQsIKzagTgFHl8F0NiSVtGXt50oidm/CRBIhrbHfC0NrVGFsVcJmrpjh3RwI4HkifC+TdAJbOdrZm7Gy2Pv6yFl2A1GP0ZMrUHje02DIu5KlACNxJwu6kj3E8DpUpoiGlv2aqtPvCcHGRJ/AWCK5V4wZlMvIn14iaCnpcjlAAAAAASUVORK5CYII="; 20 | } 21 | 22 | public static class Texture 23 | { 24 | public const string Back = "iVBORw0KGgoAAAANSUhEUgAAAAgAAAAICAYAAADED76LAAAAM0lEQVQY033MsQ0AMAgDwYf9d3YqFJDAbs/6kCT+gjmlQ4B0WIcTe2FFINJhL6xYhxMBHi+qDAxapajWAAAAAElFTkSuQmCC"; 25 | } 26 | } 27 | } -------------------------------------------------------------------------------- /Editor/ChronoHelper.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using UnityEditor; 4 | using UnityEngine; 5 | using dotsquid.ChronoHelper.Internal; 6 | 7 | namespace dotsquid.ChronoHelper 8 | { 9 | internal class ChronoHelper : EditorWindow, IHasCustomMenu 10 | { 11 | private static readonly Vector2 kBorderPositionOffset = Vector2.one; 12 | private static readonly Vector2 kBorderSizeOffset = kBorderPositionOffset * 2.0f; 13 | 14 | private class ChronoBack : IDisposable 15 | { 16 | private const float kTau = Mathf.PI * 2.0f; 17 | 18 | public enum Mode 19 | { 20 | Normal, 21 | Warning, 22 | } 23 | 24 | private ChronoHelper _owner; 25 | private Mode _mode; 26 | private double _modeSetTime; 27 | private float _blinkPhase; 28 | 29 | public Mode mode 30 | { 31 | get => _mode; 32 | set 33 | { 34 | if (value != _mode) 35 | { 36 | _mode = value; 37 | _modeSetTime = EditorApplication.timeSinceStartup; 38 | } 39 | } 40 | } 41 | 42 | private Color color 43 | { 44 | get 45 | { 46 | var setting = Settings.I; 47 | var normalColor = setting.normalBackColor; 48 | switch (_mode) 49 | { 50 | case Mode.Warning: 51 | var warningColor = setting.warningBackColor; 52 | return Color.Lerp(normalColor, warningColor, _blinkPhase); 53 | 54 | case Mode.Normal: 55 | default: 56 | return normalColor; 57 | } 58 | } 59 | } 60 | 61 | private TextureStorage textureStorage => _owner._textureStorage; 62 | 63 | public ChronoBack(ChronoHelper owner) 64 | { 65 | _owner = owner; 66 | EditorApplication.update += Update; 67 | } 68 | 69 | public void Dispose() 70 | { 71 | EditorApplication.update -= Update; 72 | } 73 | 74 | public void Draw(Rect windowRect) 75 | { 76 | using (GUIHelper.ReplaceColor.With(color)) 77 | { 78 | var backTexture = textureStorage.backTexture; 79 | var backRect = windowRect; 80 | backRect.position = kBorderPositionOffset; 81 | backRect.size -= kBorderSizeOffset; 82 | var uvRect = default(Rect); 83 | uvRect.width = backRect.width / backTexture.width; 84 | uvRect.height = backRect.height / backTexture.height; 85 | GUI.DrawTextureWithTexCoords(backRect, backTexture, uvRect); 86 | } 87 | } 88 | 89 | private void Update() 90 | { 91 | if (_mode == Mode.Warning) 92 | { 93 | var setting = Settings.I; 94 | var time = EditorApplication.timeSinceStartup; 95 | var warningPeriod = setting.warningBlinkPeriod; 96 | var warningDuration = setting.warningBlinkDuration; 97 | var endTime = _modeSetTime + warningDuration; 98 | if (time > endTime) 99 | { 100 | mode = Mode.Normal; 101 | } 102 | else 103 | { 104 | var phase = (endTime - EditorApplication.timeSinceStartup) / warningPeriod; 105 | _blinkPhase = (warningDuration > 0.0f) 106 | ? 0.5f - (float)Math.Sin(phase * kTau) * 0.5f 107 | : 1.0f; 108 | } 109 | } 110 | } 111 | } 112 | 113 | private class ChronoButton 114 | { 115 | private ChronoHelper _owner; 116 | private GUIContent _editModeContent; 117 | private GUIContent _playModeContent; 118 | private GUIContent _editModePressedContent; 119 | private GUIContent _playModePressedContent; 120 | private float _value; 121 | private bool _currentState = false; 122 | private bool _oldState = false; 123 | 124 | public bool state 125 | { 126 | get => _currentState; 127 | set 128 | { 129 | _currentState = value; 130 | if (!_currentState && _owner._currentChronoButton == this) 131 | _owner._currentChronoButton = null; 132 | if (!_oldState && _currentState) // button was pressed in 133 | { 134 | if (null != _owner._currentChronoButton) 135 | _owner._currentChronoButton.state = false; 136 | _owner._currentChronoButton = this; 137 | data.chronoScale = Mathf.Clamp(_value, kChronoMinScale, kChronoMaxScale); 138 | } 139 | _oldState = _currentState; 140 | } 141 | } 142 | 143 | public float value => _value; 144 | 145 | private Data data => _owner._data; 146 | private GUIContent normalContent => _owner._isPlayMode ? _playModeContent : _editModeContent; 147 | private GUIContent pressedContent => _owner._isPlayMode ? _playModePressedContent : _editModePressedContent; 148 | private GUIContent content => state ? pressedContent : normalContent; 149 | 150 | public ChronoButton(ChronoHelper owner, float value, GUIContent normalContent, GUIContent pressedContent = null) 151 | { 152 | _owner = owner; 153 | _value = value; 154 | _playModeContent = new GUIContent(normalContent); 155 | _editModeContent = new GUIContent(normalContent); 156 | if (null == pressedContent) 157 | { 158 | _playModePressedContent = new GUIContent(normalContent); 159 | _editModePressedContent = new GUIContent(normalContent); 160 | } 161 | else 162 | { 163 | _playModePressedContent = new GUIContent(pressedContent); 164 | _editModePressedContent = new GUIContent(pressedContent); 165 | } 166 | _editModeContent.tooltip = kEditModeTooltip; 167 | } 168 | 169 | public void Update() 170 | { 171 | state = (Mathf.Abs(data.chronoScale - _value) <= Mathf.Min(_value * 0.05f, 0.05f)); 172 | if (state) 173 | data.chronoScale = _value; 174 | } 175 | 176 | public void Draw(string style = null) 177 | { 178 | if (string.IsNullOrEmpty(style)) 179 | style = kButtonStyle; 180 | state = GUILayout.Toggle(state, content, style, _owner._controlButtonWidth, kChronoButtonHeight); 181 | } 182 | } 183 | 184 | [Serializable] 185 | private class Data 186 | { 187 | private const string kPrefKey = Consts.kNamePrefix + "data"; 188 | 189 | public float chronoScale = 1.0f; 190 | public bool canResetOnPlayEnd = true; 191 | public bool canSuppressTimeScale = false; 192 | 193 | public void Save() 194 | { 195 | var json = JsonUtility.ToJson(this); 196 | EditorPrefs.SetString(kPrefKey, json); 197 | } 198 | 199 | public void Load() 200 | { 201 | if (EditorPrefs.HasKey(kPrefKey)) 202 | { 203 | var json = EditorPrefs.GetString(kPrefKey); 204 | JsonUtility.FromJsonOverwrite(json, this); 205 | } 206 | } 207 | } 208 | 209 | private class TextureStorage 210 | { 211 | public Texture2D backTexture; 212 | public Texture2D settingsIconDarkTexture; 213 | public Texture2D settingsIconLightTexture; 214 | public Texture2D resetIconDarkTexture; 215 | public Texture2D resetIconLightTexture; 216 | public Texture2D pauseIconDarkTexture; 217 | public Texture2D pauseIconLightTexture; 218 | public Texture2D lockedIconLightTexture; 219 | public Texture2D unlockedIconDarkTexture; 220 | public Texture2D unlockedIconLightTexture; 221 | public Texture2D autoResetOnIconLightTexture; 222 | public Texture2D autoResetOffIconDarkTexture; 223 | public Texture2D autoResetOffIconLightTexture; 224 | 225 | private List _all = new List(); 226 | 227 | public void Load() 228 | { 229 | Load(out backTexture, Base64Image.Texture.Back, "CH_Texture_Back_Stripes"); 230 | Load(out settingsIconDarkTexture, Base64Image.Icon.Settings_dark, "CH_Icon_Settings_Dark"); 231 | Load(out settingsIconLightTexture, Base64Image.Icon.Settings_light, "CH_Icon_Settings_Light"); 232 | Load(out resetIconDarkTexture, Base64Image.Icon.Reset_dark, "CH_Icon_Reset_Dark"); 233 | Load(out resetIconLightTexture, Base64Image.Icon.Reset_light, "CH_Icon_Reset_Light"); 234 | Load(out pauseIconDarkTexture, Base64Image.Icon.Pause_dark, "CH_Icon_Pause_Dark"); 235 | Load(out pauseIconLightTexture, Base64Image.Icon.Pause_light, "CH_Icon_Pause_Light"); 236 | Load(out lockedIconLightTexture, Base64Image.Icon.Locked_light, "CH_Icon_Locked_Light"); 237 | Load(out unlockedIconDarkTexture, Base64Image.Icon.Unlocked_dark, "CH_Icon_Unlocked_Dark"); 238 | Load(out unlockedIconLightTexture, Base64Image.Icon.Unlocked_light, "CH_Icon_Unlocked_Light"); 239 | Load(out autoResetOnIconLightTexture, Base64Image.Icon.AutoResetOn_light, "CH_Icon_AutoResetOn_Light"); 240 | Load(out autoResetOffIconDarkTexture, Base64Image.Icon.AutoResetOff_dark, "CH_Icon_AutoResetOff_Dark"); 241 | Load(out autoResetOffIconLightTexture, Base64Image.Icon.AutoResetOff_light, "CH_Icon_AutoResetOff_Light"); 242 | } 243 | 244 | public void Clear() 245 | { 246 | foreach (var texture in _all) 247 | { 248 | DestroyImmediate(texture); 249 | } 250 | _all.Clear(); 251 | } 252 | 253 | private void Load(out Texture2D texture, string base64, string name) 254 | { 255 | texture = Helper.CreateTextureFromBase64(base64, name); 256 | _all.Add(texture); 257 | } 258 | } 259 | 260 | #region Constants 261 | private const float kChronoMinScale = 0.0f; 262 | private const float kChronoMaxScale = 100.0f; 263 | private const float kChronoButtonDefaultWidth = 38.0f; 264 | private const float kHorizontalLayoutThresholdFactor = 1.2f; 265 | private const float kHorizontalLayoutThresholdExtra = 128.0f; 266 | private const float kHorizontalLayoutHeight = 24.0f; 267 | private const float kVerticalLayoutUndockedHeight = 40.0f; 268 | private const float kVerticalLayoutDockedHeight = 44.0f; 269 | private const float kWindowMinWidth = 156.0f; 270 | private const float kWindowMaxWidth = 8192.0f; 271 | 272 | private const string kButtonStyle = "Button"; 273 | private const string kButtonMidStyle = "ButtonMid"; 274 | private const string kButtonLeftStyle = "ButtonLeft"; 275 | private const string kButtonRightStyle = "ButtonRight"; 276 | private const string kEditModeTooltip = "Does not affect Time.timeScale while in EditorMode"; 277 | private const string kMenuPath = "Window/ChronoHelper"; 278 | 279 | private static readonly GUIContent kSettingsMenuItemContent = new GUIContent("Settings"); 280 | private static readonly GUIContent kGitHubMenuItemContent = new GUIContent("GitHub"); 281 | private static readonly GUIContent kHomepageMenuItemContent = new GUIContent("Home page"); 282 | private static readonly GUIContent kDotsquidDotComMenuItemContent = new GUIContent("dotsquid.com"); 283 | private static readonly GUIContent kResetButtonContent = new GUIContent(string.Empty, "Reset"); 284 | private static readonly GUIContent kPauseButtonNormalContent = new GUIContent(string.Empty, "Pause"); 285 | private static readonly GUIContent kPauseButtonPressedContent = new GUIContent(string.Empty, "Pause"); 286 | private static readonly GUIContent kSettingsButtonContent = new GUIContent(string.Empty, "Settings"); 287 | private static readonly GUIContent kLockedButtonContent = new GUIContent(string.Empty, "Suppress Time.timeScale changes from without"); 288 | private static readonly GUIContent kUnlockedButtonContent = new GUIContent(string.Empty, "Allow Time.timeScale changes from without"); 289 | private static readonly GUIContent kAutoResetOnButtonContent = new GUIContent(string.Empty, "Auto-reset chronoScale to value set in EditorMode"); 290 | private static readonly GUIContent kAutoResetOffButtonContent = new GUIContent(string.Empty, "Don't auto-reset chronoScale to value set in EditorMode"); 291 | private static readonly GUIContent kPlayModeTooltipContent = new GUIContent(); 292 | private static readonly GUIContent kEditModeTooltipContent = new GUIContent("", "Does not affect Time.timeScale while in EditorMode"); 293 | private static readonly GUILayoutOption kChronoButtonHeight = GUILayout.Height(20.0f); 294 | private static readonly GUILayoutOption kControlToggleWidth = GUILayout.Width(24.0f); 295 | private static readonly GUILayoutOption kControlToggleHeight = GUILayout.Height(16.0f); 296 | private static readonly GUILayoutOption kControlToggleExpandWidth = GUILayout.ExpandWidth(false); 297 | private static readonly GUILayoutOption kChronoSliderValueWidth = GUILayout.Width(40.0f); 298 | private static readonly GUILayoutOption kChronoSliderMaxWidth = GUILayout.MaxWidth(8192.0f); 299 | private static readonly GUILayoutOption kChronoSliderExpandWidth = GUILayout.ExpandWidth(true); 300 | private static readonly GUILayoutOption[] kControlToggleOptions = new GUILayoutOption[] { kControlToggleWidth, kControlToggleHeight, kControlToggleExpandWidth }; 301 | #endregion 302 | 303 | private Data _data = new Data(); 304 | private TextureStorage _textureStorage = new TextureStorage(); 305 | private SettingsWindow _settingsWindow; 306 | private bool _isPlayMode = false; 307 | private bool _areChronoButtonDirty = true; 308 | private float _originalTimeScale = 1.0f; 309 | private float _originalChronoScale = 1.0f; 310 | private float _chronoButtonWidth = kChronoButtonDefaultWidth; 311 | private float _chronoButtonsTotalWidth = 0.0f; 312 | private Layout _currentLayout; 313 | private Rect _windowRect; 314 | private ChronoBack _chronoBack; 315 | private ChronoButton[] _chronoButtons; 316 | private ChronoButton _currentChronoButton = null; 317 | private GUILayoutOption _controlButtonWidth = GUILayout.Width(kChronoButtonDefaultWidth); 318 | 319 | private float maxChronoValue 320 | { 321 | get 322 | { 323 | float result = 1.0f; 324 | if (null != _chronoButtons) 325 | { 326 | var count = _chronoButtons.Length; 327 | if (count > 0) 328 | { 329 | result = _chronoButtons[count - 1].value; 330 | } 331 | } 332 | return result; 333 | } 334 | } 335 | 336 | [MenuItem(kMenuPath, false, 25000)] 337 | private static void ShowWindow() 338 | { 339 | GetWindow(); 340 | } 341 | 342 | private void Awake() 343 | { 344 | _data.Load(); 345 | UpdatePlayModeState(); 346 | } 347 | 348 | private void OnEnable() 349 | { 350 | titleContent = new GUIContent("ChronoHelper"); 351 | _textureStorage.Load(); 352 | UpdateButtonsContent(); 353 | InitChronoBack(); 354 | ScheduleChronoButtonsRecreation(); 355 | Subscribe(); 356 | } 357 | 358 | private void OnDisable() 359 | { 360 | _chronoBack.Dispose(); 361 | _textureStorage.Clear(); 362 | CloseSettingsWindow(); 363 | Unsubscribe(); 364 | } 365 | 366 | private void OnDestroy() 367 | { 368 | _data.Save(); 369 | ResetTimeScale(); 370 | } 371 | 372 | private void OnInspectorUpdate() 373 | { 374 | Repaint(); 375 | } 376 | 377 | private void OnGUI() 378 | { 379 | RecreateChronoButtonsIfRequired(); 380 | CheckChronoScaleIntegrity(); 381 | UpdateChronoScale(); 382 | UpdateWindowRect(); 383 | DrawBackground(); 384 | DrawContextMenu(); 385 | DrawLayout(); 386 | UpdateTimeScale(); 387 | } 388 | 389 | private void InitChronoBack() 390 | { 391 | _chronoBack = new ChronoBack(this); 392 | } 393 | 394 | private void UpdateButtonsContent() 395 | { 396 | if (EditorGUIUtility.isProSkin) 397 | { 398 | kSettingsButtonContent.image = _textureStorage.settingsIconLightTexture; 399 | kResetButtonContent.image = _textureStorage.resetIconLightTexture; 400 | kPauseButtonNormalContent.image = _textureStorage.pauseIconLightTexture; 401 | kPauseButtonPressedContent.image = _textureStorage.pauseIconLightTexture; 402 | kLockedButtonContent.image = _textureStorage.lockedIconLightTexture; 403 | kUnlockedButtonContent.image = _textureStorage.unlockedIconLightTexture; 404 | kAutoResetOnButtonContent.image = _textureStorage.autoResetOnIconLightTexture; 405 | kAutoResetOffButtonContent.image = _textureStorage.autoResetOffIconLightTexture; 406 | } 407 | else 408 | { 409 | kSettingsButtonContent.image = _textureStorage.settingsIconDarkTexture; 410 | kResetButtonContent.image = _textureStorage.resetIconDarkTexture; 411 | kPauseButtonNormalContent.image = _textureStorage.pauseIconDarkTexture; 412 | kPauseButtonPressedContent.image = _textureStorage.pauseIconLightTexture; 413 | kLockedButtonContent.image = _textureStorage.lockedIconLightTexture; 414 | kUnlockedButtonContent.image = _textureStorage.unlockedIconDarkTexture; 415 | kAutoResetOnButtonContent.image = _textureStorage.autoResetOnIconLightTexture; 416 | kAutoResetOffButtonContent.image = _textureStorage.autoResetOffIconDarkTexture; 417 | } 418 | } 419 | 420 | private void ResetChronoButtonWidth() 421 | { 422 | _chronoButtonWidth = kChronoButtonDefaultWidth; 423 | } 424 | 425 | private void UpdateChronoButtonWidth(GUIContent buttonContent) 426 | { 427 | GUIStyle buttonStyle = kButtonStyle; 428 | buttonStyle.CalcMinMaxWidth(buttonContent, out var minWidth, out var maxWidth); 429 | _chronoButtonWidth = Mathf.Max(_chronoButtonWidth, maxWidth); 430 | } 431 | 432 | private void ApplyChronoButtonWidth() 433 | { 434 | var buttonsWidth = Settings.I.buttonWidth; 435 | switch (buttonsWidth) 436 | { 437 | case ButtonWidth.Equal: 438 | _controlButtonWidth = GUILayout.Width(_chronoButtonWidth); 439 | break; 440 | 441 | case ButtonWidth.AsIs: 442 | _controlButtonWidth = GUILayout.MaxWidth(8192.0f); 443 | break; 444 | } 445 | } 446 | 447 | private void ScheduleChronoButtonsRecreation() 448 | { 449 | _areChronoButtonDirty = true; 450 | } 451 | 452 | private void RecreateChronoButtonsIfRequired() 453 | { 454 | if (!_areChronoButtonDirty) 455 | return; 456 | 457 | ResetChronoButtonWidth(); 458 | 459 | var settings = Settings.I; 460 | var points = settings.chronoPointList.array; 461 | var mode = settings.buttonFormat; 462 | int count = points.Length; 463 | 464 | _chronoButtons = new ChronoButton[count]; 465 | for (int i = 0; i < count; ++i) 466 | { 467 | var point = points[i]; 468 | var value = point.value; 469 | GUIContent normalContent = null; 470 | GUIContent pressedContent = null; 471 | if (Mathf.Approximately(value, 0.0f)) 472 | { 473 | normalContent = kPauseButtonNormalContent; 474 | pressedContent = kPauseButtonPressedContent; 475 | } 476 | else 477 | { 478 | var display = ChronoValueFormatter.Nicify(value, mode); 479 | normalContent = new GUIContent(display); 480 | } 481 | _chronoButtons[i] = new ChronoButton(this, value, normalContent, pressedContent); 482 | UpdateChronoButtonWidth(normalContent); 483 | } 484 | UpdateChronoButtonWidth(kResetButtonContent); 485 | ApplyChronoButtonWidth(); 486 | _areChronoButtonDirty = false; 487 | } 488 | 489 | private void DrawBackground() 490 | { 491 | _chronoBack.Draw(_windowRect); 492 | } 493 | 494 | private void DrawLayout() 495 | { 496 | float viewWidth = EditorGUIUtility.currentViewWidth; 497 | switch (Settings.I.layout) 498 | { 499 | case Layout.Auto: 500 | if (viewWidth > _chronoButtonsTotalWidth * kHorizontalLayoutThresholdFactor + kHorizontalLayoutThresholdExtra) 501 | { 502 | DrawHorizontalLayout(); 503 | } 504 | else 505 | { 506 | DrawVerticalLayout(); 507 | } 508 | break; 509 | 510 | case Layout.Vertical: 511 | DrawVerticalLayout(); 512 | break; 513 | 514 | case Layout.Horizontal: 515 | DrawHorizontalLayout(); 516 | break; 517 | } 518 | } 519 | 520 | private void DrawHorizontalLayout() 521 | { 522 | _currentLayout = Layout.Horizontal; 523 | minSize = new Vector2(kWindowMinWidth, kHorizontalLayoutHeight); 524 | maxSize = new Vector2(kWindowMaxWidth, kHorizontalLayoutHeight); 525 | 526 | using (new EditorGUILayout.HorizontalScope()) 527 | { 528 | switch (Settings.I.blockOrder) 529 | { 530 | case BlockOrder.Reversed: 531 | DrawChronoSliderInHorizontalLayout(); 532 | DrawControlButtons(); 533 | break; 534 | 535 | case BlockOrder.Normal: 536 | default: 537 | DrawControlButtons(); 538 | DrawChronoSliderInHorizontalLayout(); 539 | break; 540 | } 541 | } 542 | } 543 | 544 | private void DrawVerticalLayout() 545 | { 546 | _currentLayout = Layout.Vertical; 547 | 548 | bool isDocked = true; 549 | #if UNITY_2020_1_OR_NEWER 550 | isDocked = docked; 551 | #endif 552 | if (isDocked) 553 | { 554 | minSize = new Vector2(kWindowMinWidth, kVerticalLayoutDockedHeight); 555 | maxSize = new Vector2(kWindowMaxWidth, kVerticalLayoutDockedHeight); 556 | } 557 | else 558 | { 559 | minSize = new Vector2(kWindowMinWidth, kVerticalLayoutUndockedHeight); 560 | maxSize = new Vector2(kWindowMaxWidth, kVerticalLayoutUndockedHeight); 561 | } 562 | 563 | using (new EditorGUILayout.VerticalScope()) 564 | { 565 | switch (Settings.I.blockOrder) 566 | { 567 | case BlockOrder.Reversed: 568 | DrawControlButtons(); 569 | DrawChronoSlider(); 570 | break; 571 | 572 | case BlockOrder.Normal: 573 | default: 574 | DrawChronoSlider(); 575 | GUILayout.Space(-1.0f); 576 | DrawControlButtons(); 577 | break; 578 | } 579 | } 580 | } 581 | 582 | private void DrawChronoSliderInHorizontalLayout() 583 | { 584 | using (new EditorGUILayout.VerticalScope()) 585 | { 586 | GUILayout.Space(5.0f); 587 | DrawChronoSlider(); 588 | } 589 | } 590 | 591 | private void DrawChronoSlider() 592 | { 593 | const float kMinLinearValue = 0.0f; 594 | const float kMaxLinearValue = 2.0f; 595 | float maxChronoValue = this.maxChronoValue; 596 | 597 | using (new EditorGUILayout.HorizontalScope()) 598 | { 599 | var oldChronoScale = _data.chronoScale; 600 | var content = _isPlayMode ? kPlayModeTooltipContent : kEditModeTooltipContent; 601 | 602 | var linear = ChronoScaleToLinear(_data.chronoScale, maxChronoValue); 603 | linear = GUILayout.HorizontalSlider(linear, kMinLinearValue, kMaxLinearValue, kChronoSliderExpandWidth, kChronoSliderMaxWidth); 604 | _data.chronoScale = ChronoLinearToScale(linear, maxChronoValue); 605 | 606 | float roundChronoScale = (float)Math.Round(_data.chronoScale, 3); 607 | _data.chronoScale = EditorGUILayout.FloatField(roundChronoScale, kChronoSliderValueWidth); 608 | 609 | DrawTooltipOverLastRect(content); 610 | if (oldChronoScale != _data.chronoScale) 611 | { 612 | UpdateChronoButtons(); 613 | } 614 | DrawControlToggles(); 615 | } 616 | } 617 | 618 | private void DrawControlButtons() 619 | { 620 | var settings = Settings.I; 621 | using (new EditorGUILayout.HorizontalScope()) 622 | { 623 | if (settings.blockOrder == BlockOrder.Normal || 624 | _currentLayout == Layout.Vertical) 625 | { 626 | GUILayout.Space(4.0f); 627 | } 628 | GUILayout.FlexibleSpace(); 629 | 630 | using (new EditorGUILayout.HorizontalScope()) 631 | { 632 | if (settings.showResetButton) 633 | { 634 | using (new EditorGUI.DisabledScope(!Application.isPlaying)) 635 | { 636 | if (GUILayout.Button(kResetButtonContent, kButtonLeftStyle, _controlButtonWidth, kChronoButtonHeight)) 637 | ResetTimeScale(); 638 | } 639 | } 640 | 641 | for (int i = 0, count = _chronoButtons.Length; i < count; ++i) 642 | { 643 | string style = kButtonMidStyle; 644 | if (i == 0 && !Settings.I.showResetButton) 645 | style = kButtonLeftStyle; 646 | else if (i == count - 1) 647 | style = kButtonRightStyle; 648 | 649 | _chronoButtons[i].Draw(style); 650 | } 651 | } 652 | UpdateChronoButtonsTotalWidth(); 653 | UpdateChronoButtons(); 654 | 655 | GUILayout.FlexibleSpace(); 656 | } 657 | } 658 | 659 | private void DrawControlToggles() 660 | { 661 | using (new EditorGUILayout.HorizontalScope()) 662 | { 663 | { 664 | var autoResetButtonContent = _data.canResetOnPlayEnd 665 | ? kAutoResetOnButtonContent 666 | : kAutoResetOffButtonContent; 667 | _data.canResetOnPlayEnd = GUILayout.Toggle(_data.canResetOnPlayEnd, autoResetButtonContent, EditorStyles.miniButtonLeft, kControlToggleOptions); 668 | } 669 | 670 | { 671 | var suppressButtonContent = _data.canSuppressTimeScale 672 | ? kLockedButtonContent 673 | : kUnlockedButtonContent; 674 | _data.canSuppressTimeScale = GUILayout.Toggle(_data.canSuppressTimeScale, suppressButtonContent, EditorStyles.miniButtonMid, kControlToggleOptions); 675 | } 676 | 677 | if (GUILayout.Button(kSettingsButtonContent, EditorStyles.miniButtonRight, kControlToggleOptions)) 678 | { 679 | OpenSettingsWindow(); 680 | } 681 | } 682 | } 683 | 684 | private void DrawContextMenu() 685 | { 686 | var evt = Event.current; 687 | if (evt.type == EventType.ContextClick) 688 | { 689 | var mousePos = evt.mousePosition; 690 | if (_windowRect.Contains(mousePos)) 691 | { 692 | var menu = new GenericMenu(); 693 | PopulateMenu(menu); 694 | menu.ShowAsContext(); 695 | evt.Use(); 696 | } 697 | } 698 | } 699 | 700 | private void OpenSettingsWindow() 701 | { 702 | _settingsWindow = SettingsWindow.Open(); 703 | } 704 | 705 | private void CloseSettingsWindow() 706 | { 707 | if (_settingsWindow != null) 708 | _settingsWindow.Close(); 709 | } 710 | 711 | private void UpdateChronoButtonsTotalWidth() 712 | { 713 | float lastWidth = GUILayoutUtility.GetLastRect().width; 714 | if (lastWidth > 1.0f) 715 | _chronoButtonsTotalWidth = lastWidth; 716 | } 717 | 718 | private void UpdateWindowRect() 719 | { 720 | _windowRect = new Rect(Vector2.zero, position.size); 721 | } 722 | 723 | private void CheckChronoScaleIntegrity() 724 | { 725 | if (_isPlayMode && EditorApplication.isPlaying) 726 | { 727 | var settings = Settings.I; 728 | var warningMode = settings.warningMode; 729 | bool isIntegrityViolated = (_data.chronoScale != Time.timeScale); 730 | bool showWarning = false; 731 | switch (warningMode) 732 | { 733 | case WarningMode.WhenNotSuppressing: 734 | if (!_data.canSuppressTimeScale) 735 | showWarning = true; 736 | break; 737 | 738 | case WarningMode.Always: 739 | showWarning = true; 740 | break; 741 | } 742 | if (showWarning && isIntegrityViolated) 743 | { 744 | _chronoBack.mode = ChronoBack.Mode.Warning; 745 | } 746 | } 747 | } 748 | 749 | private void UpdateChronoScale() 750 | { 751 | if (_isPlayMode && 752 | !_data.canSuppressTimeScale && 753 | EditorApplication.isPlaying) 754 | { 755 | _data.chronoScale = Time.timeScale; 756 | } 757 | } 758 | 759 | private void UpdateTimeScale() 760 | { 761 | if (_isPlayMode) 762 | Time.timeScale = _data.chronoScale; 763 | } 764 | 765 | private void UpdateChronoButtons() 766 | { 767 | for (int i = 0; i < _chronoButtons.Length; ++i) 768 | { 769 | _chronoButtons[i].Update(); 770 | } 771 | } 772 | 773 | private void StoreTimeScale() 774 | { 775 | _originalTimeScale = Time.timeScale; 776 | _originalChronoScale = _data.chronoScale; 777 | } 778 | 779 | private void ResetTimeScale() 780 | { 781 | Time.timeScale = _originalTimeScale; 782 | if (_data.canResetOnPlayEnd) 783 | { 784 | _data.chronoScale = _originalChronoScale; 785 | } 786 | } 787 | 788 | private void UpdatePlayModeState() 789 | { 790 | var oldPlayMode = _isPlayMode; 791 | _isPlayMode = EditorApplication.isPlaying; 792 | if (_isPlayMode && !oldPlayMode) // EditMode -> PlayMode 793 | { 794 | StoreTimeScale(); 795 | UpdateTimeScale(); 796 | } 797 | else if (!_isPlayMode && oldPlayMode) // PlayMode -> EditMode 798 | { 799 | ResetTimeScale(); 800 | } 801 | } 802 | 803 | private void PopulateMenu(GenericMenu menu) 804 | { 805 | menu.AddItem(kSettingsMenuItemContent, false, OpenSettingsWindow); 806 | menu.AddItem(kGitHubMenuItemContent, false, () => Application.OpenURL(Consts.URL.Github)); 807 | menu.AddItem(kHomepageMenuItemContent, false, () => Application.OpenURL(Consts.URL.Homepage)); 808 | menu.AddItem(kDotsquidDotComMenuItemContent, false, () => Application.OpenURL(Consts.URL.DotsquidDotCom)); 809 | } 810 | 811 | void IHasCustomMenu.AddItemsToMenu(GenericMenu menu) 812 | { 813 | PopulateMenu(menu); 814 | } 815 | 816 | private void OnApplicationStateChanged(PlayModeStateChange state) 817 | { 818 | switch (state) 819 | { 820 | case PlayModeStateChange.ExitingEditMode: 821 | _isPlayMode = true; 822 | StoreTimeScale(); 823 | UpdateTimeScale(); 824 | break; 825 | 826 | case PlayModeStateChange.EnteredEditMode: 827 | _isPlayMode = false; 828 | ResetTimeScale(); 829 | break; 830 | } 831 | } 832 | 833 | private void OnChronoButtonsDirty() 834 | { 835 | ScheduleChronoButtonsRecreation(); 836 | } 837 | 838 | private void OnChronoWarningTest() 839 | { 840 | _chronoBack.mode = ChronoBack.Mode.Warning; 841 | } 842 | 843 | private void Subscribe() 844 | { 845 | SettingsWindow.onChronoButtonsDirty += OnChronoButtonsDirty; 846 | SettingsWindow.onChronoWarningTest += OnChronoWarningTest; 847 | EditorApplication.playModeStateChanged += OnApplicationStateChanged; 848 | } 849 | 850 | private void Unsubscribe() 851 | { 852 | SettingsWindow.onChronoButtonsDirty -= OnChronoButtonsDirty; 853 | SettingsWindow.onChronoWarningTest -= OnChronoWarningTest; 854 | EditorApplication.playModeStateChanged -= OnApplicationStateChanged; 855 | } 856 | 857 | private static void DrawTooltipOverLastRect(GUIContent content) 858 | { 859 | var lastRect = GUILayoutUtility.GetLastRect(); 860 | GUI.Label(lastRect, content); 861 | } 862 | 863 | private static float ChronoScaleToLinear(float scale, float max) 864 | { 865 | if (scale <= 1.0f) 866 | return scale; 867 | else 868 | return Mathf.InverseLerp(1.0f, max, scale) + 1.0f; 869 | } 870 | 871 | private static float ChronoLinearToScale(float linear, float max) 872 | { 873 | if (linear <= 1.0f) 874 | return linear; 875 | else 876 | return Mathf.Lerp(1.0f, max, linear - 1.0f); 877 | } 878 | } 879 | } 880 | --------------------------------------------------------------------------------