diff --git a/GamecraftModdingAPI/Interface/IMGUI/Button.cs b/GamecraftModdingAPI/Interface/IMGUI/Button.cs new file mode 100644 index 0000000..5a119f6 --- /dev/null +++ b/GamecraftModdingAPI/Interface/IMGUI/Button.cs @@ -0,0 +1,92 @@ +using System; +using Unity.Mathematics; +using UnityEngine; + +namespace GamecraftModdingAPI.Interface.IMGUI +{ + /// + /// A clickable button. + /// This wraps Unity's IMGUI Button. + /// + public class Button : UIElement + { + private bool automaticLayout = false; + + private string text; + + /// + /// The rectangular area that the button can use. + /// + public Rect Box { get; set; } = Rect.zero; + + /// + /// An event that fires when the button is clicked. + /// + public event EventHandler OnClick; + + public void OnGUI() + { + if (automaticLayout) + { + if (GUILayout.Button(text, Constants.Default.button)) + { + OnClick?.Invoke(this, true); + } + } + else + { + if (GUI.Button(Box, text, Constants.Default.button)) + { + OnClick?.Invoke(this, true); + } + } + } + + /// + /// The button's unique name. + /// + public string Name { get; private set; } + + /// + /// Whether to display the button. + /// + public bool Enabled { get; set; } = true; + + /// + /// Initialize a new button with automatic layout. + /// + /// The text to display on the button. + /// The button's name. + public Button(string text, string name = null) + { + automaticLayout = true; + this.text = text; + if (name == null) + { + this.Name = typeof(Button).FullName + "::" + text; + } + else + { + this.Name = name; + } + IMGUIManager.AddElement(this); + } + + /// + /// Initialize a new button. + /// + /// The text to display on the button. + /// Rectangular area for the button to use. + /// The button's name. + public Button(string text, Rect box, string name = null) : this(text, name) + { + automaticLayout = false; + this.Box = box; + } + + ~Button() + { + IMGUIManager.RemoveElement(this); + } + } +} \ No newline at end of file diff --git a/GamecraftModdingAPI/Interface/IMGUI/Constants.cs b/GamecraftModdingAPI/Interface/IMGUI/Constants.cs new file mode 100644 index 0000000..3e2fa8f --- /dev/null +++ b/GamecraftModdingAPI/Interface/IMGUI/Constants.cs @@ -0,0 +1,168 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Globalization; +using System.Reflection; +using GamecraftModdingAPI.Utility; +using HarmonyLib; +using Svelto.Tasks; +using Svelto.Tasks.Lean; +using UnityEngine; +using UnityEngine.AddressableAssets; +using UnityEngine.ResourceManagement.AsyncOperations; + +namespace GamecraftModdingAPI.Interface.IMGUI +{ + /// + /// Convenient IMGUI values. + /// + public static class Constants + { + private static byte _defaultCompletion = 0; + private static GUISkin _default = null; + + /// + /// Best-effort imitation of Gamecraft's UI style. + /// + public static GUISkin Default + { + get + { + if (_defaultCompletion != 0) _default = BuildDefaultGUISkin(); + return _default; + } + } + + private static Font _riffic = null; + + private static Texture2D _blueBackground = null; + private static Texture2D _grayBackground = null; + private static Texture2D _whiteBackground = null; + private static Texture2D _textInputBackground = null; + private static Texture2D _areaBackground = null; + + internal static void Init() + { + LoadGUIAssets(); + } + + private static GUISkin BuildDefaultGUISkin() + { + _defaultCompletion = 0; + if (_riffic == null) return GUI.skin; + // build GUISkin + GUISkin gui = ScriptableObject.CreateInstance(); + gui.font = _riffic; + gui.settings.selectionColor = Color.white; + gui.settings.tripleClickSelectsLine = true; + // set properties off all UI elements + foreach (PropertyInfo p in typeof(GUISkin).GetProperties()) + { + // for a "scriptable" GUI system, it's ironic there's no better way to do this + if (p.GetValue(gui) is GUIStyle style) + { + style.richText = true; + style.alignment = TextAnchor.MiddleCenter; + style.fontSize = 30; + style.wordWrap = true; + style.border = new RectOffset(4, 4, 4, 4); + style.margin = new RectOffset(4, 4, 4, 4); + style.padding = new RectOffset(4, 4, 4, 4); + // normal state + style.normal.background = _blueBackground; + style.normal.textColor = Color.white; + // hover state + style.hover.background = _grayBackground; + style.hover.textColor = Color.white; + // focused + style.focused.background = _grayBackground; + style.focused.textColor = Color.white; + // clicking state + style.active.background = _whiteBackground; + style.active.textColor = Color.white; + + p.SetValue(gui, style); // probably unnecessary + } + } + // set element-specific styles + // label + gui.label.normal.background = null; + gui.label.hover.background = null; + gui.label.focused.background = null; + gui.label.active.background = null; + // text input + gui.textField.normal.background = _textInputBackground; + gui.textField.hover.background = _textInputBackground; + gui.textField.focused.background = _textInputBackground; + gui.textField.active.background = _textInputBackground; + // text area + gui.textArea.normal.background = _textInputBackground; + gui.textArea.hover.background = _textInputBackground; + gui.textArea.focused.background = _textInputBackground; + gui.textArea.active.background = _textInputBackground; + // window + gui.window.normal.background = _areaBackground; + gui.window.hover.background = _areaBackground; + gui.window.focused.background = _areaBackground; + gui.window.active.background = _areaBackground; + // box (also used by layout groups & areas) + gui.box.normal.background = _areaBackground; + gui.box.hover.background = _areaBackground; + gui.box.focused.background = _areaBackground; + gui.box.active.background = _areaBackground; + return gui; + } + + private static void LoadGUIAssets() + { + AsyncOperationHandle rifficHandle = Addressables.LoadAssetAsync("Assets/Art/Fonts/riffic-bold.ttf"); + rifficHandle.Completed += handle => + { + _riffic = handle.Result; + _defaultCompletion++; + }; + _blueBackground = new Texture2D(1, 1); + _blueBackground.SetPixel(0, 0, new Color(0.004f, 0.522f, 0.847f) /* Gamecraft Blue */); + _blueBackground.Apply(); + _grayBackground = new Texture2D(1, 1); + _grayBackground.SetPixel(0, 0, new Color(0.745f, 0.745f, 0.745f) /* Gray */); + _grayBackground.Apply(); + _whiteBackground = new Texture2D(1, 1); + _whiteBackground.SetPixel(0, 0, new Color(0.898f, 0.898f, 0.898f) /* Very light gray */); + _whiteBackground.Apply(); + _textInputBackground = new Texture2D(1, 1); + _textInputBackground.SetPixel(0, 0, new Color(0f, 0f, 0f, 0.25f) /* Translucent gray */); + _textInputBackground.Apply(); + _areaBackground = new Texture2D(1, 1); + _areaBackground.SetPixel(0, 0, new Color(0f, 0f, 0f, 0.25f) /* Translucent gray */); + _areaBackground.Apply(); + /* // this is actually gray (used for the loading screen) + AsyncOperationHandle backgroundHandle = + Addressables.LoadAssetAsync("Assets/Art/Textures/UI/FrontEndMap/RCX_Blue_Background_5k.jpg"); + backgroundHandle.Completed += handle => + { + _blueBackground = handle.Result; + _defaultCompletion++; + };*/ + _defaultCompletion++; + } + } + + [HarmonyPatch(typeof(FMODUnity.RuntimeManager), "PlayOneShot", new []{ typeof(Guid), typeof(Vector3)})] + public class FMODRuntimeManagerPlayOneShotPatch + { + public static void Prefix(Guid guid) + { + Logging.MetaLog($"Playing sound with guid '{guid.ToString()}'"); + } + } + + [HarmonyPatch(typeof(FMODUnity.RuntimeManager), "PlayOneShot", new []{ typeof(string), typeof(Vector3)})] + public class FMODRuntimeManagerPlayOneShotPatch2 + { + public static void Prefix(string path) + { + Logging.MetaLog($"Playing sound with str '{path}'"); + } + } +} \ No newline at end of file diff --git a/GamecraftModdingAPI/Interface/IMGUI/Group.cs b/GamecraftModdingAPI/Interface/IMGUI/Group.cs new file mode 100644 index 0000000..7033dee --- /dev/null +++ b/GamecraftModdingAPI/Interface/IMGUI/Group.cs @@ -0,0 +1,157 @@ +using System; +using GamecraftModdingAPI.Utility; +using Svelto.DataStructures; +using UnityEngine; + +namespace GamecraftModdingAPI.Interface.IMGUI +{ + /// + /// A group of elements. + /// This wraps Unity's GUILayout Area and GUI Group system. + /// + public class Group : UIElement + { + private bool automaticLayout; + + private FasterList elements = new FasterList(); + + /// + /// The rectangular area in the window that the UI group can use + /// + public Rect Box { get; set; } + + public void OnGUI() + { + /*if (Constants.Default == null) return; + if (Constants.Default.box == null) return;*/ + GUIStyle guiStyle = Constants.Default.box; + UIElement[] elems = elements.ToArrayFast(out uint count); + if (automaticLayout) + { + GUILayout.BeginArea(Box, guiStyle); + for (uint i = 0; i < count; i++) + { + /*try + { + if (elems[i].Enabled) + elems[i].OnGUI(); + } + catch (ArgumentException) + { + // ignore these, since this is (hopefully) just Unity being dumb + } + catch (Exception e) + { + Logging.MetaDebugLog($"Element '{elems[i].Name}' threw exception:\n{e.ToString()}"); + }*/ + if (elems[i].Enabled) + elems[i].OnGUI(); + } + GUILayout.EndArea(); + } + else + { + GUI.BeginGroup(Box, guiStyle); + for (uint i = 0; i < count; i++) + { + if (elems[i].Enabled) + elems[i].OnGUI(); + } + GUI.EndGroup(); + } + } + + /// + /// The group's unique name. + /// + public string Name { get; } + + /// + /// Whether to display the group and everything in it. + /// + public bool Enabled { set; get; } = true; + + /// + /// The amount of elements in the group. + /// + public int Length + { + get => elements.count; + } + + /// + /// Initializes a new instance of the class. + /// + /// The rectangular area to use in the window. + /// Name of the group. + /// Whether to use automatic UI layout. + public Group(Rect box, string name = null, bool automaticLayout = false) + { + Box = box; + if (name == null) + { + this.Name = typeof(Group).FullName + "::" + box.ToString().Replace(" ", ""); + } + else + { + this.Name = name; + } + this.automaticLayout = automaticLayout; + IMGUIManager.AddElement(this); + } + + /// + /// Add an element to the group. + /// + /// The element to add. + /// Index of the new element. + public int AddElement(UIElement element) + { + IMGUIManager.RemoveElement(element); // groups manage internal elements themselves + elements.Add(element); + return elements.count - 1; + } + + /// + /// Remove an element from the group. + /// + /// The element to remove. + /// Whether removal was successful. + public bool RemoveElement(UIElement element) + { + int index = IndexOf(element); + return RemoveAt(index); + } + + /// + /// Remove the element in a specific location. + /// + /// Index of the element. + /// Whether removal was successful. + public bool RemoveAt(int index) + { + if (index < 0 || index >= elements.count) return false; + IMGUIManager.AddElement(elements[index]); // re-add to global manager + elements.RemoveAt(index); + return true; + } + + /// + /// Get the index of an element. + /// + /// The element to search for. + /// The element's index, or -1 if not found. + public int IndexOf(UIElement element) + { + UIElement[] elems = elements.ToArrayFast(out uint count); + for (int i = 0; i < count; i++) + { + if (elems[i].Name == element.Name) + { + return i; + } + } + return -1; + } + } +} \ No newline at end of file diff --git a/GamecraftModdingAPI/Interface/IMGUI/IMGUIManager.cs b/GamecraftModdingAPI/Interface/IMGUI/IMGUIManager.cs new file mode 100644 index 0000000..f6048e7 --- /dev/null +++ b/GamecraftModdingAPI/Interface/IMGUI/IMGUIManager.cs @@ -0,0 +1,94 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using GamecraftModdingAPI.App; +using GamecraftModdingAPI.Utility; +using Rewired.Internal; +using Svelto.DataStructures; +using Svelto.Tasks; +using Svelto.Tasks.ExtraLean; +using Svelto.Tasks.ExtraLean.Unity; +using UnityEngine; + +namespace GamecraftModdingAPI.Interface.IMGUI +{ + public static class IMGUIManager + { + internal static OnGuiRunner ImguiScheduler = new OnGuiRunner("GamecraftModdingAPI_IMGUIScheduler"); + + private static FasterDictionary _activeElements = new FasterDictionary(); + + public static void AddElement(UIElement e) + { + if (!ExistsElement(e)) + { + _activeElements[e.Name] = e; + } + } + + public static bool ExistsElement(string name) + { + return _activeElements.ContainsKey(name); + } + + public static bool ExistsElement(UIElement element) + { + return ExistsElement(element.Name); + } + + public static bool RemoveElement(string name) + { + if (ExistsElement(name)) + { + return _activeElements.Remove(name); + } + + return false; + } + + public static bool RemoveElement(UIElement element) + { + return RemoveElement(element.Name); + } + + private static void OnGUI() + { + UIElement[] elements = _activeElements.GetValuesArray(out uint count); + for(uint i = 0; i < count; i++) + { + if (elements[i].Enabled) + elements[i].OnGUI(); + /*try + { + if (elements[i].Enabled) + elements[i].OnGUI(); + } + catch (ArgumentException) + { + // ignore these, since this is (hopefully) just Unity being dumb + } + catch (Exception e) + { + Logging.MetaDebugLog($"Element '{elements[i].Name}' threw exception:\n{e.ToString()}"); + }*/ + + } + } + + private static IEnumerator OnGUIAsync() + { + yield return (new Svelto.Tasks.Enumerators.WaitForSecondsEnumerator(5)).Continue(); // wait for some startup + while (true) + { + yield return Yield.It; + GUI.skin = Constants.Default; + OnGUI(); + } + } + + internal static void Init() + { + OnGUIAsync().RunOn(ImguiScheduler); + } + } +} \ No newline at end of file diff --git a/GamecraftModdingAPI/Interface/IMGUI/Image.cs b/GamecraftModdingAPI/Interface/IMGUI/Image.cs new file mode 100644 index 0000000..8c026ed --- /dev/null +++ b/GamecraftModdingAPI/Interface/IMGUI/Image.cs @@ -0,0 +1,58 @@ +using UnityEngine; + +namespace GamecraftModdingAPI.Interface.IMGUI +{ + public class Image : UIElement + { + private bool automaticLayout = false; + + public Texture Texture { get; set; } + + public Rect Box { get; set; } = Rect.zero; + + public void OnGUI() + { + //if (Texture == null) return; + if (automaticLayout) + { + GUILayout.Label(Texture, Constants.Default.label); + } + else + { + GUI.Label(Box, Texture, Constants.Default.label); + } + } + + public string Name { get; } + public bool Enabled { set; get; } = true; + + public Image(Texture texture = null, string name = null) + { + automaticLayout = true; + Texture = texture; + if (name == null) + { + if (texture == null) + { + this.Name = typeof(Image).FullName + "::" + texture; + } + else + { + this.Name = typeof(Image).FullName + "::" + texture.name + "(" + texture.width + "x" + texture.height + ")"; + } + + } + else + { + this.Name = name; + } + IMGUIManager.AddElement(this); + } + + public Image(Rect box, Texture texture = null, string name = null) : this(texture, name) + { + this.Box = box; + automaticLayout = false; + } + } +} \ No newline at end of file diff --git a/GamecraftModdingAPI/Interface/IMGUI/Label.cs b/GamecraftModdingAPI/Interface/IMGUI/Label.cs new file mode 100644 index 0000000..2fb54f7 --- /dev/null +++ b/GamecraftModdingAPI/Interface/IMGUI/Label.cs @@ -0,0 +1,59 @@ +using UnityEngine; + +namespace GamecraftModdingAPI.Interface.IMGUI +{ + /// + /// A simple text label. + /// This wraps Unity IMGUI's Label. + /// + public class Label : UIElement + { + private bool automaticLayout = false; + + /// + /// String to display on the label. + /// + public string Text { get; set; } + + /// + /// The rectangular area that the label can use. + /// + public Rect Box { get; set; } = Rect.zero; + + public void OnGUI() + { + if (automaticLayout) + { + GUILayout.Label(Text, Constants.Default.label); + } + else + { + GUI.Label(Box, Text, Constants.Default.label); + } + } + + public string Name { get; } + public bool Enabled { set; get; } = true; + + public Label(string initialText = null, string name = null) + { + automaticLayout = true; + Text = initialText; + if (name == null) + { + this.Name = typeof(Label).FullName + "::" + initialText; + } + else + { + this.Name = name; + } + IMGUIManager.AddElement(this); + } + + public Label(Rect box, string initialText = null, string name = null) : this(initialText, name) + { + this.Box = box; + automaticLayout = false; + } + } +} \ No newline at end of file diff --git a/GamecraftModdingAPI/Interface/IMGUI/Text.cs b/GamecraftModdingAPI/Interface/IMGUI/Text.cs new file mode 100644 index 0000000..1b94804 --- /dev/null +++ b/GamecraftModdingAPI/Interface/IMGUI/Text.cs @@ -0,0 +1,109 @@ +using System; +using UnityEngine; + +namespace GamecraftModdingAPI.Interface.IMGUI +{ + /// + /// A text input field. + /// This wraps Unity's IMGUI TextField and TextArea. + /// + public class Text : UIElement + { + private bool automaticLayout; + + /// + /// Whether the text input field is multiline (true -> TextArea) or not (false -> TextField). + /// + public bool Multiline { get; set; } + + private string text; + + /// + /// The rectangular area that the text field can use. + /// + public Rect Box { get; set; } = Rect.zero; + + /// + /// An event that fires whenever the text input is edited. + /// + public event EventHandler OnEdit; + + public void OnGUI() + { + string editedText = null; + if (automaticLayout) + { + if (Multiline) + { + editedText = GUILayout.TextArea(text, Constants.Default.textArea); + } + else + { + editedText = GUILayout.TextField(text, Constants.Default.textField); + } + } + else + { + if (Multiline) + { + editedText = GUI.TextArea(Box, text, Constants.Default.textArea); + } + else + { + editedText = GUI.TextField(Box, text, Constants.Default.textField); + } + } + + if (editedText != null && editedText != text) + { + OnEdit?.Invoke(this, editedText); + text = editedText; + } + } + + /// + /// The text field's unique name. + /// + public string Name { get; } + + /// + /// Whether to display the text field. + /// + public bool Enabled { set; get; } = true; + + /// + /// Initialize the text input field with automatic layout. + /// + /// Initial text in the input field. + /// The text field's name. + /// Allow multiple lines? + public Text(string initialText = null, string name = null, bool multiline = false) + { + this.Multiline = multiline; + automaticLayout = true; + text = initialText ?? ""; + if (name == null) + { + this.Name = typeof(Text).FullName + "::" + text; + } + else + { + this.Name = name; + } + IMGUIManager.AddElement(this); + } + + /// + /// Initialize the text input field. + /// + /// Rectangular area for the text field. + /// Initial text in the input field. + /// The text field's name. + /// Allow multiple lines? + public Text(Rect box, string initialText = null, string name = null, bool multiline = false) : this(initialText, name, multiline) + { + this.Box = box; + automaticLayout = false; + } + } +} \ No newline at end of file diff --git a/GamecraftModdingAPI/Interface/IMGUI/UIElement.cs b/GamecraftModdingAPI/Interface/IMGUI/UIElement.cs new file mode 100644 index 0000000..abaed6e --- /dev/null +++ b/GamecraftModdingAPI/Interface/IMGUI/UIElement.cs @@ -0,0 +1,28 @@ +using System; + +namespace GamecraftModdingAPI.Interface.IMGUI +{ + /// + /// GUI Element like a text field, button or picture. + /// This interface is used to wrap many elements from Unity's IMGUI system. + /// + public interface UIElement + { + /// + /// GUI operations to perform in the OnGUI scope. + /// This is basically equivalent to a MonoBehaviour's OnGUI method. + /// + void OnGUI(); + + /// + /// The element's name. + /// This should be unique for every instance of the class. + /// + string Name { get; } + + /// + /// Whether to display the UI element or not. + /// + bool Enabled { get; } + } +} \ No newline at end of file diff --git a/GamecraftModdingAPI/Main.cs b/GamecraftModdingAPI/Main.cs index 584ae90..d957a46 100644 --- a/GamecraftModdingAPI/Main.cs +++ b/GamecraftModdingAPI/Main.cs @@ -89,6 +89,9 @@ namespace GamecraftModdingAPI AsyncUtils.Init(); GamecraftModdingAPI.App.Client.Init(); GamecraftModdingAPI.App.Game.Init(); + // init UI + Interface.IMGUI.Constants.Init(); + Interface.IMGUI.IMGUIManager.Init(); Logging.MetaLog($"{currentAssembly.GetName().Name} v{currentAssembly.GetName().Version} initialized"); } diff --git a/GamecraftModdingAPI/Tests/GamecraftModdingAPIPluginTest.cs b/GamecraftModdingAPI/Tests/GamecraftModdingAPIPluginTest.cs index 81a814c..e3abdca 100644 --- a/GamecraftModdingAPI/Tests/GamecraftModdingAPIPluginTest.cs +++ b/GamecraftModdingAPI/Tests/GamecraftModdingAPIPluginTest.cs @@ -5,7 +5,7 @@ using System.Linq; using System.Reflection; using System.Reflection.Emit; using System.Text; - +using GamecraftModdingAPI.App; using HarmonyLib; using IllusionInjector; // test @@ -22,8 +22,16 @@ using GamecraftModdingAPI.Commands; using GamecraftModdingAPI.Events; using GamecraftModdingAPI.Utility; using GamecraftModdingAPI.Blocks; +using GamecraftModdingAPI.Interface.IMGUI; using GamecraftModdingAPI.Players; +using UnityEngine.AddressableAssets; +using UnityEngine.AddressableAssets.ResourceLocators; +using UnityEngine.ResourceManagement.AsyncOperations; +using UnityEngine.ResourceManagement.ResourceLocations; +using UnityEngine.ResourceManagement.ResourceProviders; +using Debug = FMOD.Debug; using EventType = GamecraftModdingAPI.Events.EventType; +using Label = GamecraftModdingAPI.Interface.IMGUI.Label; namespace GamecraftModdingAPI.Tests { @@ -347,13 +355,38 @@ namespace GamecraftModdingAPI.Tests { Logging.Log("Compatible GamecraftScripting detected"); } + // Interface test + /*Interface.IMGUI.Group uiGroup = new Group(new Rect(20, 20, 200, 500), "GamecraftModdingAPI_UITestGroup", true); + Interface.IMGUI.Button button = new Button("TEST"); + button.OnClick += (b, __) => { Logging.MetaDebugLog($"Click on {((Interface.IMGUI.Button)b).Name}");}; + Interface.IMGUI.Button button2 = new Button("TEST2"); + button2.OnClick += (b, __) => { Logging.MetaDebugLog($"Click on {((Interface.IMGUI.Button)b).Name}");}; + Text uiText = new Text("This is text!", multiline: true); + uiText.OnEdit += (t, txt) => { Logging.MetaDebugLog($"Text in {((Text)t).Name} is now '{txt}'"); }; + Label uiLabel = new Label("Label!"); + Image uiImg = new Image(name:"Behold this texture!"); + uiImg.Enabled = false; + uiGroup.AddElement(button); + uiGroup.AddElement(button2); + uiGroup.AddElement(uiText); + uiGroup.AddElement(uiLabel); + uiGroup.AddElement(uiImg); + Addressables.LoadAssetAsync("Assets/Art/Textures/UI/FrontEndMap/RCX_Blue_Background_5k.jpg") + .Completed += + handle => + { + uiImg.Texture = handle.Result; + uiImg.Enabled = true; + Logging.MetaDebugLog($"Got blue bg asset {handle.Result}"); + }; + */ #if TEST TestRoot.RunTests(); #endif } - private string modsString; + private string modsString; private string InstalledMods() { if (modsString != null) return modsString;