Browse Source

Add IMGUI styling and initial OOP implementation

tags/v1.8.0
NGnius (Graham) 3 years ago
parent
commit
1c014e36ac
10 changed files with 804 additions and 3 deletions
  1. +92
    -0
      GamecraftModdingAPI/Interface/IMGUI/Button.cs
  2. +168
    -0
      GamecraftModdingAPI/Interface/IMGUI/Constants.cs
  3. +157
    -0
      GamecraftModdingAPI/Interface/IMGUI/Group.cs
  4. +94
    -0
      GamecraftModdingAPI/Interface/IMGUI/IMGUIManager.cs
  5. +58
    -0
      GamecraftModdingAPI/Interface/IMGUI/Image.cs
  6. +59
    -0
      GamecraftModdingAPI/Interface/IMGUI/Label.cs
  7. +109
    -0
      GamecraftModdingAPI/Interface/IMGUI/Text.cs
  8. +28
    -0
      GamecraftModdingAPI/Interface/IMGUI/UIElement.cs
  9. +3
    -0
      GamecraftModdingAPI/Main.cs
  10. +36
    -3
      GamecraftModdingAPI/Tests/GamecraftModdingAPIPluginTest.cs

+ 92
- 0
GamecraftModdingAPI/Interface/IMGUI/Button.cs View File

@@ -0,0 +1,92 @@
using System;
using Unity.Mathematics;
using UnityEngine;

namespace GamecraftModdingAPI.Interface.IMGUI
{
/// <summary>
/// A clickable button.
/// This wraps Unity's IMGUI Button.
/// </summary>
public class Button : UIElement
{
private bool automaticLayout = false;

private string text;
/// <summary>
/// The rectangular area that the button can use.
/// </summary>
public Rect Box { get; set; } = Rect.zero;

/// <summary>
/// An event that fires when the button is clicked.
/// </summary>
public event EventHandler<bool> 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);
}
}
}

/// <summary>
/// The button's unique name.
/// </summary>
public string Name { get; private set; }

/// <summary>
/// Whether to display the button.
/// </summary>
public bool Enabled { get; set; } = true;

/// <summary>
/// Initialize a new button with automatic layout.
/// </summary>
/// <param name="text">The text to display on the button.</param>
/// <param name="name">The button's name.</param>
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);
}

/// <summary>
/// Initialize a new button.
/// </summary>
/// <param name="text">The text to display on the button.</param>
/// <param name="box">Rectangular area for the button to use.</param>
/// <param name="name">The button's name.</param>
public Button(string text, Rect box, string name = null) : this(text, name)
{
automaticLayout = false;
this.Box = box;
}
~Button()
{
IMGUIManager.RemoveElement(this);
}
}
}

+ 168
- 0
GamecraftModdingAPI/Interface/IMGUI/Constants.cs View File

@@ -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
{
/// <summary>
/// Convenient IMGUI values.
/// </summary>
public static class Constants
{
private static byte _defaultCompletion = 0;
private static GUISkin _default = null;
/// <summary>
/// Best-effort imitation of Gamecraft's UI style.
/// </summary>
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<GUISkin>();
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<Font> rifficHandle = Addressables.LoadAssetAsync<Font>("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<Texture2D> backgroundHandle =
Addressables.LoadAssetAsync<Texture2D>("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}'");
}
}
}

+ 157
- 0
GamecraftModdingAPI/Interface/IMGUI/Group.cs View File

@@ -0,0 +1,157 @@
using System;
using GamecraftModdingAPI.Utility;
using Svelto.DataStructures;
using UnityEngine;

namespace GamecraftModdingAPI.Interface.IMGUI
{
/// <summary>
/// A group of elements.
/// This wraps Unity's GUILayout Area and GUI Group system.
/// </summary>
public class Group : UIElement
{
private bool automaticLayout;
private FasterList<UIElement> elements = new FasterList<UIElement>();

/// <summary>
/// The rectangular area in the window that the UI group can use
/// </summary>
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();
}
}

/// <summary>
/// The group's unique name.
/// </summary>
public string Name { get; }
/// <summary>
/// Whether to display the group and everything in it.
/// </summary>
public bool Enabled { set; get; } = true;
/// <summary>
/// The amount of elements in the group.
/// </summary>
public int Length
{
get => elements.count;
}

/// <summary>
/// Initializes a new instance of the <see cref="T:GamecraftModdingAPI.Interface.IMGUI.Group"/> class.
/// </summary>
/// <param name="box">The rectangular area to use in the window.</param>
/// <param name="name">Name of the group.</param>
/// <param name="automaticLayout">Whether to use automatic UI layout.</param>
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);
}

/// <summary>
/// Add an element to the group.
/// </summary>
/// <param name="element">The element to add.</param>
/// <returns>Index of the new element.</returns>
public int AddElement(UIElement element)
{
IMGUIManager.RemoveElement(element); // groups manage internal elements themselves
elements.Add(element);
return elements.count - 1;
}

/// <summary>
/// Remove an element from the group.
/// </summary>
/// <param name="element">The element to remove.</param>
/// <returns>Whether removal was successful.</returns>
public bool RemoveElement(UIElement element)
{
int index = IndexOf(element);
return RemoveAt(index);
}

/// <summary>
/// Remove the element in a specific location.
/// </summary>
/// <param name="index">Index of the element.</param>
/// <returns>Whether removal was successful.</returns>
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;
}

/// <summary>
/// Get the index of an element.
/// </summary>
/// <param name="element">The element to search for.</param>
/// <returns>The element's index, or -1 if not found.</returns>
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;
}
}
}

+ 94
- 0
GamecraftModdingAPI/Interface/IMGUI/IMGUIManager.cs View File

@@ -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<string, UIElement> _activeElements = new FasterDictionary<string,UIElement>();

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<TaskContract> 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);
}
}
}

+ 58
- 0
GamecraftModdingAPI/Interface/IMGUI/Image.cs View File

@@ -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;
}
}
}

+ 59
- 0
GamecraftModdingAPI/Interface/IMGUI/Label.cs View File

@@ -0,0 +1,59 @@
using UnityEngine;

namespace GamecraftModdingAPI.Interface.IMGUI
{
/// <summary>
/// A simple text label.
/// This wraps Unity IMGUI's Label.
/// </summary>
public class Label : UIElement
{
private bool automaticLayout = false;

/// <summary>
/// String to display on the label.
/// </summary>
public string Text { get; set; }
/// <summary>
/// The rectangular area that the label can use.
/// </summary>
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;
}
}
}

+ 109
- 0
GamecraftModdingAPI/Interface/IMGUI/Text.cs View File

@@ -0,0 +1,109 @@
using System;
using UnityEngine;

namespace GamecraftModdingAPI.Interface.IMGUI
{
/// <summary>
/// A text input field.
/// This wraps Unity's IMGUI TextField and TextArea.
/// </summary>
public class Text : UIElement
{
private bool automaticLayout;

/// <summary>
/// Whether the text input field is multiline (true -> TextArea) or not (false -> TextField).
/// </summary>
public bool Multiline { get; set; }

private string text;
/// <summary>
/// The rectangular area that the text field can use.
/// </summary>
public Rect Box { get; set; } = Rect.zero;

/// <summary>
/// An event that fires whenever the text input is edited.
/// </summary>
public event EventHandler<string> 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;
}
}

/// <summary>
/// The text field's unique name.
/// </summary>
public string Name { get; }
/// <summary>
/// Whether to display the text field.
/// </summary>
public bool Enabled { set; get; } = true;

/// <summary>
/// Initialize the text input field with automatic layout.
/// </summary>
/// <param name="initialText">Initial text in the input field.</param>
/// <param name="name">The text field's name.</param>
/// <param name="multiline">Allow multiple lines?</param>
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);
}

/// <summary>
/// Initialize the text input field.
/// </summary>
/// <param name="box">Rectangular area for the text field.</param>
/// <param name="initialText">Initial text in the input field.</param>
/// <param name="name">The text field's name.</param>
/// <param name="multiline">Allow multiple lines?</param>
public Text(Rect box, string initialText = null, string name = null, bool multiline = false) : this(initialText, name, multiline)
{
this.Box = box;
automaticLayout = false;
}
}
}

+ 28
- 0
GamecraftModdingAPI/Interface/IMGUI/UIElement.cs View File

@@ -0,0 +1,28 @@
using System;

namespace GamecraftModdingAPI.Interface.IMGUI
{
/// <summary>
/// GUI Element like a text field, button or picture.
/// This interface is used to wrap many elements from Unity's IMGUI system.
/// </summary>
public interface UIElement
{
/// <summary>
/// GUI operations to perform in the OnGUI scope.
/// This is basically equivalent to a MonoBehaviour's OnGUI method.
/// </summary>
void OnGUI();
/// <summary>
/// The element's name.
/// This should be unique for every instance of the class.
/// </summary>
string Name { get; }
/// <summary>
/// Whether to display the UI element or not.
/// </summary>
bool Enabled { get; }
}
}

+ 3
- 0
GamecraftModdingAPI/Main.cs View File

@@ -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");
}



+ 36
- 3
GamecraftModdingAPI/Tests/GamecraftModdingAPIPluginTest.cs View File

@@ -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<Texture2D>("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;


Loading…
Cancel
Save