diff --git a/CommunityEntity.UI.Test.cs b/CommunityEntity.UI.Test.cs index 87bff2c..a6a7fba 100644 --- a/CommunityEntity.UI.Test.cs +++ b/CommunityEntity.UI.Test.cs @@ -6,14 +6,13 @@ #if SERVER public class cui { - [ServerUserVar] - public static void test( ConsoleSystem.Arg args ) - { - const string json = @"[ + [ServerUserVar] + public static void cui_test( ConsoleSystem.Arg args ) + { + const string json = @"[ { ""name"": ""TestPanel7766"", ""parent"": ""Overlay"", - ""components"": [ { @@ -22,22 +21,19 @@ public static void test( ConsoleSystem.Arg args ) ""color"": ""1.0 1.0 1.0 1.0"", ""url"": ""http://files.facepunch.com/garry/2015/June/03/2015-06-03_12-19-17.jpg"", }, - { ""type"":""RectTransform"", ""anchormin"": ""0 0"", ""anchormax"": ""1 1"" }, - { ""type"":""NeedsCursor"" } ] }, - { ""parent"": ""TestPanel7766"", - + ""name"": ""buttonText"", ""components"": [ { @@ -46,7 +42,6 @@ public static void test( ConsoleSystem.Arg args ) ""fontSize"":32, ""align"": ""MiddleCenter"", }, - { ""type"":""RectTransform"", ""anchormin"": ""0 0.5"", @@ -54,11 +49,9 @@ public static void test( ConsoleSystem.Arg args ) } ] }, - { ""name"": ""Button88"", ""parent"": ""TestPanel7766"", - ""components"": [ { @@ -68,7 +61,6 @@ public static void test( ConsoleSystem.Arg args ) ""color"": ""0.9 0.8 0.3 0.8"", ""imagetype"": ""Tiled"" }, - { ""type"":""RectTransform"", ""anchormin"": ""0.3 0.15"", @@ -76,10 +68,8 @@ public static void test( ConsoleSystem.Arg args ) } ] }, - { ""parent"": ""Button88"", - ""components"": [ { @@ -89,29 +79,151 @@ public static void test( ConsoleSystem.Arg args ) ""align"": ""MiddleCenter"" } ] - } + }, + { + ""name"": ""ItemIcon"", + ""parent"": ""TestPanel7766"", + ""components"": + [ + { + ""type"":""UnityEngine.UI.Image"", + ""color"": ""1.0 1.0 1.0 1.0"", + ""imagetype"": ""Simple"", + ""itemid"": -151838493, + }, + { + ""type"":""RectTransform"", + ""anchormin"":""0.4 0.4"", + ""anchormax"":""0.4 0.4"", + ""offsetmin"": ""-32 -32"", + ""offsetmax"": ""32 32"" + } + ] + }, + { + ""name"": ""ItemIconSkinTest"", + ""parent"": ""TestPanel7766"", + ""components"": + [ + { + ""type"":""UnityEngine.UI.Image"", + ""color"": ""1.0 1.0 1.0 1.0"", + ""imagetype"": ""Simple"", + ""itemid"": -733625651, + ""skinid"": 13035 + }, + { + ""type"":""RectTransform"", + ""anchormin"":""0.6 0.6"", + ""anchormax"":""0.6 0.6"", + ""offsetmin"": ""-32 -32"", + ""offsetmax"": ""32 32"" + } + ] + }, + { + ""name"": ""UpdateLabelTest"", + ""parent"": ""TestPanel7766"", + ""components"": + [ + { + ""type"":""UnityEngine.UI.Text"", + ""text"":""This should go away once you update!"", + ""fontSize"":32, + ""align"": ""MiddleRight"", + }, + ] + }, + ] + "; + CommunityEntity.ServerInstance.ClientRPCEx( new Network.SendInfo() { connection = args.Connection }, null, "AddUI", json ); + } + + [ServerUserVar] + public static void cui_test_update( ConsoleSystem.Arg args ) + { + //adding an allowUpdate field will try and update components based on the name + //if a gameObject isn't named then update will not work + //only fields that are included will be updated, so it's not necessary to pass every variable + //for instance, the below update json will update the background image and text of the test command above + const string json = @"[ + { + ""name"": ""TestPanel7766"", + ""update"": true, + ""components"": + [ + { + ""type"":""UnityEngine.UI.RawImage"", + ""url"": ""https://files.facepunch.com/paddy/20220405/zipline_01.jpg"", + }, + ] + }, + { + ""name"": ""buttonText"", + ""update"": true, + ""components"": + [ + { + ""type"":""UnityEngine.UI.Text"", + ""text"":""This text just got updated!"", + }, + ] + }, + { + ""name"": ""ItemIcon"", + ""update"": true, + ""components"": + [ + { + ""type"":""UnityEngine.UI.Image"", + ""itemid"": -2067472972, + }, + ] + }, + { + ""name"": ""Button88"", + ""update"": true, + ""components"": + [ + { + ""type"":""UnityEngine.UI.Button"", + ""color"": ""0.9 0.3 0.3 0.8"", + }, + ] + }, + { + ""name"": ""UpdateLabelTest"", + ""update"": true, + ""components"": + [ + { + ""type"":""UnityEngine.UI.Text"", + ""enabled"": false, + }, + ] + }, ] "; - CommunityEntity.ServerInstance.ClientRPCEx( new Network.SendInfo() { connection = args.Connection }, null, "AddUI", json ); - } + CommunityEntity.ServerInstance.ClientRPCEx( new Network.SendInfo() { connection = args.Connection }, null, "AddUI", json ); + } + +#if UNITY_EDITOR && CLIENT && SERVER + [UnityEditor.MenuItem( "Debug/CUI/Load custom UI" )] + public static void loadcustomjson() + { + if ( LocalPlayer.Entity == null || !Application.isPlaying ) + return; - #if UNITY_EDITOR && CLIENT && SERVER - [UnityEditor.MenuItem("Debug/CUI/Load custom UI")] - public static void loadcustomjson() - { - if(LocalPlayer.Entity == null || !Application.isPlaying) - return; - - var toLoad = UnityEditor.EditorUtility.OpenFilePanel("JSON to load", Application.dataPath, "txt"); + var toLoad = UnityEditor.EditorUtility.OpenFilePanel( "JSON to load", Application.dataPath, "txt" ); - if (!string.IsNullOrEmpty(toLoad)) - { - CommunityEntity.ServerInstance.ClientRPC( null, "AddUI", System.IO.File.ReadAllText(toLoad)); - } - } - #endif + if ( !string.IsNullOrEmpty( toLoad ) ) + { + CommunityEntity.ServerInstance.ClientRPC( null, "AddUI", System.IO.File.ReadAllText( toLoad ) ); + } + } +#endif [ServerUserVar] public static void endtest( ConsoleSystem.Arg args ) diff --git a/CommunityEntity.UI.cs b/CommunityEntity.UI.cs index f8209a6..c8154a6 100644 --- a/CommunityEntity.UI.cs +++ b/CommunityEntity.UI.cs @@ -39,13 +39,13 @@ public void SetVisible( bool b ) private static void RegisterUi( GameObject go ) { AllUi.Add( go ); - UiDict[go.name] = go; + UiDict[ go.name ] = go; } [RPC_Client] public void AddUI( RPCMessage msg ) { - if (Client.IsPlayingDemo && !ConVar.Demo.showCommunityUI) + if ( Client.IsPlayingDemo && !ConVar.Demo.showCommunityUI ) return; var str = msg.read.StringRaw(); @@ -65,26 +65,45 @@ public void AddUI( RPCMessage msg ) var parentPanel = FindPanel( json.GetString( "parent", "Overlay" ) ); if ( parentPanel == null ) { - Debug.LogWarning( "AddUI: Unknown Parent for \""+ json.GetString( "name", "AddUI CreatedPanel" ) + "\": " + json.GetString( "parent", "Overlay" ) ); + Debug.LogWarning( "[AddUI] Unknown Parent for \"" + json.GetString( "name", "AddUI CreatedPanel" ) + "\": " + json.GetString( "parent", "Overlay" ) ); return; } - var go = new GameObject( json.GetString( "name", "AddUI CreatedPanel" ) , typeof(RectTransform) ); - go.transform.SetParent( parentPanel.transform, false ); - RegisterUi( go ); + var allowUpdate = json.GetBoolean( "update", false ); + var gameObjectName = json.GetString( "name", "AddUI CreatedPanel" ); + GameObject go = null; - var rt = go.GetComponent(); - if ( rt ) + if ( allowUpdate && json.ContainsKey( "name" ) ) { - rt.anchorMin = new Vector2( 0, 0 ); - rt.anchorMax = new Vector2( 1, 1 ); - rt.offsetMin = new Vector2( 0, 0 ); - rt.offsetMax = new Vector2( 1, 1 ); + go = FindPanel( gameObjectName ); + } + + if ( allowUpdate && go == null ) + { + Debug.LogWarning( $"[AddUI] Unable to update object '{gameObjectName}': can't be found" ); + return; + } + + + if ( go == null ) + { + go = new GameObject( gameObjectName, typeof( RectTransform ) ); + go.transform.SetParent( parentPanel.transform, false ); + RegisterUi( go ); + + var rt = go.GetComponent(); + if ( rt ) + { + rt.anchorMin = new Vector2( 0, 0 ); + rt.anchorMax = new Vector2( 1, 1 ); + rt.offsetMin = new Vector2( 0, 0 ); + rt.offsetMax = new Vector2( 1, 1 ); + } } foreach ( var component in json.GetArray( "components" ) ) { - CreateComponents( go, component.Obj ); + CreateComponents( go, component.Obj, allowUpdate ); } if ( json.ContainsKey( "fadeOut" ) ) @@ -108,8 +127,33 @@ private GameObject FindPanel( string name ) return null; } - private void CreateComponents( GameObject go, JSON.Object obj ) + // Move this local function outside CreateComponents() + private static void HandleEnableState( JSON.Object json, Behaviour component ) + { + if ( json.TryGetBoolean( "enabled", out var result ) ) + { + component.enabled = result; + } + } + + private void CreateComponents( GameObject go, JSON.Object obj, bool allowUpdate ) { + // Unsure if local functions allocate like lambdas do, this is just for modding so not a big deal & can double check once it builds + bool ShouldUpdateField( string fieldName ) + { + return !allowUpdate || obj.ContainsKey( fieldName ); + } + + T GetOrAddComponent() where T : Component + { + if ( allowUpdate && go.TryGetComponent( out T component ) ) + { + return component; + } + + return go.AddComponent(); + } + // // This is the 'stupid' but 'safe & predictable way of doing this. // @@ -117,24 +161,37 @@ private void CreateComponents( GameObject go, JSON.Object obj ) { case "UnityEngine.UI.Text": { - var c = go.AddComponent(); - c.text = obj.GetString( "text", "Text" ); - c.fontSize = obj.GetInt( "fontSize", 14 ); - c.font = FileSystem.Load( "Assets/Content/UI/Fonts/" + obj.GetString( "font", "RobotoCondensed-Bold.ttf" ) ); - c.alignment = ParseEnum( obj.GetString( "align" ), TextAnchor.UpperLeft ); - c.color = ColorEx.Parse( obj.GetString( "color", "1.0 1.0 1.0 1.0" ) ); - c.verticalOverflow = ParseEnum( obj.GetString( "verticalOverflow", "Truncate" ), VerticalWrapMode.Truncate ); + var c = GetOrAddComponent(); + HandleEnableState( obj, c ); + if ( ShouldUpdateField( "text" ) ) + c.text = obj.GetString( "text", "Text" ); + if ( ShouldUpdateField( "fontSize" ) ) + c.fontSize = obj.GetInt( "fontSize", 14 ); + if ( ShouldUpdateField( "font" ) ) + c.font = FileSystem.Load( "Assets/Content/UI/Fonts/" + obj.GetString( "font", "RobotoCondensed-Bold.ttf" ) ); + if ( ShouldUpdateField( "align" ) ) + c.alignment = ParseEnum( obj.GetString( "align" ), TextAnchor.UpperLeft ); + if ( ShouldUpdateField( "color" ) ) + c.color = ColorEx.Parse( obj.GetString( "color", "1.0 1.0 1.0 1.0" ) ); + if ( ShouldUpdateField( "verticalOverflow" ) ) + c.verticalOverflow = ParseEnum( obj.GetString( "verticalOverflow", "Truncate" ), VerticalWrapMode.Truncate ); + GraphicComponentCreated( c, obj ); break; } case "UnityEngine.UI.Image": { - var c = go.AddComponent(); - c.sprite = FileSystem.Load( obj.GetString( "sprite", "Assets/Content/UI/UI.Background.Tile.psd" ) ); - c.material = FileSystem.Load( obj.GetString( "material", "Assets/Icons/IconMaterial.mat" ) ); - c.color = ColorEx.Parse( obj.GetString( "color", "1.0 1.0 1.0 1.0" ) ); - c.type = ParseEnum( obj.GetString( "imagetype", "Simple" ), UnityEngine.UI.Image.Type.Simple ); + var c = GetOrAddComponent(); + HandleEnableState( obj, c ); + if ( ShouldUpdateField( "sprite" ) ) + c.sprite = FileSystem.Load( obj.GetString( "sprite", "Assets/Content/UI/UI.Background.Tile.psd" ) ); + if ( ShouldUpdateField( "material" ) ) + c.material = FileSystem.Load( obj.GetString( "material", "Assets/Icons/IconMaterial.mat" ) ); + if ( ShouldUpdateField( "color" ) ) + c.color = ColorEx.Parse( obj.GetString( "color", "1.0 1.0 1.0 1.0" ) ); + if ( ShouldUpdateField( "imagetype" ) ) + c.type = ParseEnum( obj.GetString( "imagetype", "Simple" ), UnityEngine.UI.Image.Type.Simple ); if ( obj.ContainsKey( "png" ) && uint.TryParse( obj.GetString( "png" ), out var id ) ) { @@ -151,7 +208,7 @@ private void CreateComponents( GameObject go, JSON.Object obj ) if ( obj.ContainsKey( "skinid" ) ) { - var requestedSkin = (ulong)obj.GetNumber("skinid" ); + var requestedSkin = (ulong)obj.GetNumber( "skinid" ); var skin = itemdef.skins.FirstOrDefault( x => x.id == (int)requestedSkin ); if ( skin.id == (int)requestedSkin ) { @@ -159,11 +216,11 @@ private void CreateComponents( GameObject go, JSON.Object obj ) } else { - var workshopSprite = WorkshopIconLoader.Find(requestedSkin, null, () => + var workshopSprite = WorkshopIconLoader.Find( requestedSkin, null, () => { - if (c != null) - c.sprite = WorkshopIconLoader.Find(requestedSkin); - }); + if ( c != null ) + c.sprite = WorkshopIconLoader.Find( requestedSkin ); + } ); if ( workshopSprite != null ) { c.sprite = workshopSprite; @@ -180,9 +237,12 @@ private void CreateComponents( GameObject go, JSON.Object obj ) case "UnityEngine.UI.RawImage": { - var c = go.AddComponent(); - c.texture = FileSystem.Load( obj.GetString( "sprite", "Assets/Icons/rust.png" ) ); - c.color = ColorEx.Parse( obj.GetString( "color", "1.0 1.0 1.0 1.0" ) ); + var c = GetOrAddComponent(); + HandleEnableState( obj, c ); + if ( ShouldUpdateField( "sprite" ) ) + c.texture = FileSystem.Load( obj.GetString( "sprite", "Assets/Icons/rust.png" ) ); + if ( ShouldUpdateField( "color" ) ) + c.color = ColorEx.Parse( obj.GetString( "color", "1.0 1.0 1.0 1.0" ) ); if ( obj.ContainsKey( "material" ) ) { @@ -206,26 +266,35 @@ private void CreateComponents( GameObject go, JSON.Object obj ) case "UnityEngine.UI.Button": { - var c = go.AddComponent(); + var c = GetOrAddComponent(); + HandleEnableState( obj, c ); if ( obj.ContainsKey( "command" ) ) { var cmd = obj.GetString( "command" ); + if ( allowUpdate ) + c.onClick.RemoveAllListeners(); c.onClick.AddListener( () => { ConsoleNetwork.ClientRunOnServer( cmd ); } ); } if ( obj.ContainsKey( "close" ) ) { var pnlName = obj.GetString( "close" ); + if ( allowUpdate ) + c.onClick.RemoveAllListeners(); c.onClick.AddListener( () => { DestroyPanel( pnlName ); } ); } // bg image - var img = go.AddComponent(); - img.sprite = FileSystem.Load( obj.GetString( "sprite", "Assets/Content/UI/UI.Background.Tile.psd" ) ); - img.material = FileSystem.Load( obj.GetString( "material", "Assets/Icons/IconMaterial.mat" ) ); - img.color = ColorEx.Parse( obj.GetString( "color", "1.0 1.0 1.0 1.0" ) ); - img.type = ParseEnum( obj.GetString( "imagetype", "Simple" ), UnityEngine.UI.Image.Type.Simple ); + var img = GetOrAddComponent(); + if ( ShouldUpdateField( "sprite" ) ) + img.sprite = FileSystem.Load( obj.GetString( "sprite", "Assets/Content/UI/UI.Background.Tile.psd" ) ); + if ( ShouldUpdateField( "material" ) ) + img.material = FileSystem.Load( obj.GetString( "material", "Assets/Icons/IconMaterial.mat" ) ); + if ( ShouldUpdateField( "color" ) ) + img.color = ColorEx.Parse( obj.GetString( "color", "1.0 1.0 1.0 1.0" ) ); + if ( ShouldUpdateField( "imagetype" ) ) + img.type = ParseEnum( obj.GetString( "imagetype", "Simple" ), UnityEngine.UI.Image.Type.Simple ); c.image = img; @@ -236,50 +305,69 @@ private void CreateComponents( GameObject go, JSON.Object obj ) case "UnityEngine.UI.Outline": { - var c = go.AddComponent(); - c.effectColor = ColorEx.Parse( obj.GetString( "color", "1.0 1.0 1.0 1.0" ) ); - c.effectDistance = Vector2Ex.Parse( obj.GetString( "distance", "1.0 -1.0" ) ); + var c = GetOrAddComponent(); + HandleEnableState( obj, c ); + if ( ShouldUpdateField( "color" ) ) + c.effectColor = ColorEx.Parse( obj.GetString( "color", "1.0 1.0 1.0 1.0" ) ); + if ( ShouldUpdateField( "distance" ) ) + c.effectDistance = Vector2Ex.Parse( obj.GetString( "distance", "1.0 -1.0" ) ); c.useGraphicAlpha = obj.ContainsKey( "useGraphicAlpha" ); break; } case "UnityEngine.UI.InputField": { - var t = go.AddComponent(); - t.fontSize = obj.GetInt( "fontSize", 14 ); - t.font = FileSystem.Load( "Assets/Content/UI/Fonts/" + obj.GetString( "font", "RobotoCondensed-Bold.ttf" ) ); - t.alignment = ParseEnum( obj.GetString( "align" ), TextAnchor.UpperLeft ); - t.color = ColorEx.Parse( obj.GetString( "color", "1.0 1.0 1.0 1.0" ) ); - - var c = go.AddComponent(); + var t = GetOrAddComponent(); + HandleEnableState( obj, t ); + if ( ShouldUpdateField( "fontSize" ) ) + t.fontSize = obj.GetInt( "fontSize", allowUpdate ? t.fontSize : 14 ); + if ( ShouldUpdateField( "font" ) ) + t.font = FileSystem.Load( "Assets/Content/UI/Fonts/" + obj.GetString( "font", "RobotoCondensed-Bold.ttf" ) ); + if ( ShouldUpdateField( "align" ) ) + t.alignment = ParseEnum( obj.GetString( "align" ), TextAnchor.UpperLeft ); + if ( ShouldUpdateField( "color" ) ) + t.color = ColorEx.Parse( obj.GetString( "color", "1.0 1.0 1.0 1.0" ) ); + + var c = GetOrAddComponent(); + HandleEnableState( obj, c ); c.textComponent = t; - c.characterLimit = obj.GetInt( "characterLimit", 0 ); + if ( ShouldUpdateField( "characterLimit" ) ) + c.characterLimit = obj.GetInt( "characterLimit", allowUpdate ? c.characterLimit : 0 ); if ( obj.ContainsKey( "command" ) ) { var cmd = obj.GetString( "command" ); + if ( allowUpdate ) + c.onEndEdit.RemoveAllListeners(); c.onEndEdit.AddListener( ( value ) => { ConsoleNetwork.ClientRunOnServer( cmd + " " + value ); } ); } - c.text = obj.GetString("text", string.Empty); - c.readOnly = obj.GetBoolean("readOnly", false); - c.lineType = ParseEnum(obj.GetString("lineType", "SingleLine"), InputField.LineType.SingleLine); - if ( obj.ContainsKey( "password" ) ) + + if ( ShouldUpdateField( "text" ) ) + c.text = obj.GetString( "text", "Text" ); + if ( ShouldUpdateField( "readOnly" ) ) + c.readOnly = obj.GetBoolean( "readOnly", false ); + if ( ShouldUpdateField( "lineType" ) ) + c.lineType = ParseEnum( obj.GetString( "lineType", "SingleLine" ), InputField.LineType.SingleLine ); + + if ( obj.TryGetBoolean( "password", out var password ) ) { - c.inputType = UnityEngine.UI.InputField.InputType.Password; + c.inputType = password ? InputField.InputType.Password : InputField.InputType.Standard; } - if (obj.ContainsKey("needsKeyboard")) + if ( obj.TryGetBoolean( "needsKeyboard", out var needsKeyboard ) ) { - go.AddComponent(); + var comp = GetOrAddComponent(); + comp.enabled = needsKeyboard; } //blocks keyboard input the same as NeedsKeyboard, but is used for UI in the inventory/crafting - if (obj.ContainsKey("hudMenuInput")) + if ( obj.TryGetBoolean( "hudMenuInput", out var hudMenuInput ) ) { - go.AddComponent(); + var comp = GetOrAddComponent(); + comp.enabled = hudMenuInput; } - if (obj.ContainsKey("autofocus")) + if ( obj.ContainsKey( "autofocus" ) ) { c.Select(); } @@ -291,7 +379,8 @@ private void CreateComponents( GameObject go, JSON.Object obj ) case "NeedsCursor": { - go.AddComponent(); + var c = GetOrAddComponent(); + HandleEnableState( obj, c ); break; } @@ -300,20 +389,28 @@ private void CreateComponents( GameObject go, JSON.Object obj ) var rt = go.GetComponent(); if ( rt ) { - rt.anchorMin = Vector2Ex.Parse( obj.GetString( "anchormin", "0.0 0.0" ) ); - rt.anchorMax = Vector2Ex.Parse( obj.GetString( "anchormax", "1.0 1.0" ) ); - rt.offsetMin = Vector2Ex.Parse( obj.GetString( "offsetmin", "0.0 0.0" ) ); - rt.offsetMax = Vector2Ex.Parse( obj.GetString( "offsetmax", "1.0 1.0" ) ); + if ( ShouldUpdateField( "anchormin" ) ) + rt.anchorMin = Vector2Ex.Parse( obj.GetString( "anchormin", "0.0 0.0" ) ); + if ( ShouldUpdateField( "anchormax" ) ) + rt.anchorMax = Vector2Ex.Parse( obj.GetString( "anchormax", "1.0 1.0" ) ); + if ( ShouldUpdateField( "offsetmin" ) ) + rt.offsetMin = Vector2Ex.Parse( obj.GetString( "offsetmin", "0.0 0.0" ) ); + if ( ShouldUpdateField( "offsetmax" ) ) + rt.offsetMax = Vector2Ex.Parse( obj.GetString( "offsetmax", "1.0 1.0" ) ); } break; } case "Countdown": { - var c = go.AddComponent(); - c.endTime = obj.GetInt( "endTime", 0 ); - c.startTime = obj.GetInt( "startTime", 0 ); - c.step = obj.GetInt( "step", 1 ); + var c = GetOrAddComponent(); + HandleEnableState( obj, c ); + if ( ShouldUpdateField( "endTime" ) ) + c.endTime = obj.GetInt( "endTime", allowUpdate ? c.endTime : 0 ); + if ( ShouldUpdateField( "startTime" ) ) + c.startTime = obj.GetInt( "startTime", allowUpdate ? c.startTime : 0 ); + if ( ShouldUpdateField( "step" ) ) + c.step = obj.GetInt( "step", allowUpdate ? c.step : 1 ); if ( obj.ContainsKey( "command" ) ) { @@ -324,17 +421,18 @@ private void CreateComponents( GameObject go, JSON.Object obj ) } case "NeedsKeyboard": { - go.AddComponent(); + var c = GetOrAddComponent(); + HandleEnableState( obj, c ); break; } } } - - private static T ParseEnum(string value, T defaultValue) + + private static T ParseEnum( string value, T defaultValue ) where T : struct, System.Enum { - if (string.IsNullOrWhiteSpace(value)) return defaultValue; - return System.Enum.TryParse(value, true, out var parsedValue) ? parsedValue : defaultValue; + if ( string.IsNullOrWhiteSpace( value ) ) return defaultValue; + return System.Enum.TryParse( value, true, out var parsedValue ) ? parsedValue : defaultValue; } private void GraphicComponentCreated( UnityEngine.UI.Graphic c, JSON.Object obj ) @@ -363,8 +461,8 @@ private IEnumerator LoadTextureFromWWW( UnityEngine.UI.RawImage c, string p ) } - Texture2D texture = new Texture2D(2, 2, TextureFormat.RGBA32, false); - www.LoadImageIntoTexture(texture); + Texture2D texture = new Texture2D( 2, 2, TextureFormat.RGBA32, false ); + www.LoadImageIntoTexture( texture ); if ( c == null ) { Debug.Log( "Error downloading image: " + p + " (not an image)" ); diff --git a/README.md b/README.md new file mode 100644 index 0000000..bd10d3e --- /dev/null +++ b/README.md @@ -0,0 +1,37 @@ +# Rust Community Repository +> This Repository is part of the game Rust. It has no relation to the Programming Language. + +Client Code for the `CommunityEntity` Class to fulfill Server-side Modder Requests. +Holds the Custom User Interface System (CUI System), which lets developers create interactive UI through a simple JSON Schema. + +## Table of Contents +> Click one of the Topics or dive directly into the Component Documentation +- [The Basics](/docs/Basics.md) +- [Components](/docs/components/README.md) + - [RectTransform](/docs/components/RectTransform.md) + - [RawImage](/docs/components/UnityEngine.UI.RawImage.md) + - [Image](/docs/components/UnityEngine.UI.Image.md) + - [Text](/docs/components/UnityEngine.UI.Text.md) + - [Outline](/docs/components/UnityEngine.UI.Outline.md) + - [Button](/docs/components/UnityEngine.UI.Button.md) + - [InputField](/docs/components/UnityEngine.UI.InputField.md) + - [NeedsCursor & NeedsKeyboard](/docs/components/NeedsX.md) + - [Countdown](/docs/components/Countdown.md) +- [Tips, Bugs & Edge cases](/docs/Bugs-Tips.md) +- [Credits & Acknowledgements](#Credits) + +## Credits +External Contributors & People who improved the CUI System +- [@bawNg](https://github.com/bawNg) - PRs [#1](https://github.com/Facepunch/Rust.Community/pull/1), [#2](https://github.com/Facepunch/Rust.Community/pull/2), [#5](https://github.com/Facepunch/Rust.Community/pull/5) +- [@balu92](https://github.com/balu92) - PRs [#7](https://github.com/Facepunch/Rust.Community/pull/7), [#9](https://github.com/Facepunch/Rust.Community/pull/9) +- [@Jake-Rich](https://github.com/Jake-Rich) - PRs [#16](https://github.com/Facepunch/Rust.Community/pull/16), [#30](https://github.com/Facepunch/Rust.Community/pull/39) +- [@Mughisi](https://github.com/Mughisi) - PRs [#19](https://github.com/Facepunch/Rust.Community/pull/19), [#22](https://github.com/Facepunch/Rust.Community/pull/22) +- [@shooter46](https://github.com/shooter46) - PR [#28](https://github.com/Facepunch/Rust.Community/pull/28) +- [@Alitop](https://github.com/Alitop) - PR [#32](https://github.com/Facepunch/Rust.Community/pull/32) +- [@TactiTac0z](https://github.com/TactiTac0z) - PR [#40](https://github.com/Facepunch/Rust.Community/pull/40) +- [@Kulltero](https://github.com/Kulltero) - PR [#43](https://github.com/Facepunch/Rust.Community/pull/43) +> And of course, Facepunch, who has put a lot of effort into giving Developers the CUI System & the ability to improve it. + +--- +### Disclaimer +The README & Documentation is Community maintained. It aims to give Developers a starting Point and educate them on effectively using CUIs for their Projects. diff --git a/docs/Basics.md b/docs/Basics.md new file mode 100644 index 0000000..ef24e5c --- /dev/null +++ b/docs/Basics.md @@ -0,0 +1,76 @@ +# The Basics of CUI +**[Back to the Start](/README.md)** | **[Next Topic](/docs/components/README.md) >** + +There are a few basic Concepts that are needed to make your own CUI, below you will learn about what Panels are & how to create and destroy UI Elements + +## The Schema + +the JSON Schema to send UI to the Player consists of a List of Elements, where each Element creates a new Panel on the Client. Elements have One or more Components that control the Look & Functionality. + +```json +[{ + "name": "AddUI CreatedPanel", + "parent": "Overlay", + "components": [...], + "fadeOut": 0.0, + "destroyUi": "" +}, ...] +``` +> The values in these JSON examples represent the default values that are assigned if no property is specified. +> … represent One or more collapsed Objects + +| Key | Type | Notes | +| :-- | :------- | :------------------- | +| `name` | string | The identifier of your panel, needed when destroying UI or adding panels inside this one | +| `parent` | string | Tells the client which Panel or Layer to Parent to. **[Needs to be a valid Panel or Layer name](/docs/Bugs-Tips.md#addui-unknown-parent-for-name--parent)** | +| `components` |List of Components | One or more Components, without these there’s no point in sending a panel | +| `fadeOut` | float | Makes the Panel fade out instead of disappearing immediately. _Currently doesn’t fade out any child panels._ | +| `destroyUi` | string | Destroys the Panel specified in the string before creating your Panel. Useful **[preventing flickering](/docs/Bugs-Tips.md#flickering-when-destroying--re-sending-ui-on-the-same-frame)** when updating UI. | + + +### About Layers +layers are used when creating your top most Panel. They differ from Panels because they are static GameObjects that cannot be destroyed via a DestroyUI call. Depending on the Layer you parent to, your UI will appear above or below Rust's own UI elements. + +#### Available Layer values +- `Overall` the top most layer in front of all of Rust's UI +- `Overlay` +- `Hud.Menu` the layer where rust positions menus like your inventory +- `Hud` the layer where Rust stores most HUD elements like your status bar +- `Under` the lowermost layer, your UI will appear behind all of Rust's UI + + +### About Naming + +It’s recommended to always name your Panels _something_, this is because the CUI System doesn’t support multiple Panels with the same name and may cause **[Ghost panels](/docs/Bugs-Tips.md#orphaned-ui-panels-ui-that-cant-be-destroyed-or-ghost-panels)** which can't be destroyed. + +It’s also recommended to prefix the Name of your Panel with something unique to your Mod, which ensures there are no accidental name Conflicts with other Mods + + +## Sending & destroying UI + +There are two RPC Calls you can use to send & destroy UI respectively + +Adding UI: +```c# +BasePlayer player = targetPlayer; +string json = "..."; // your UI in JSON form +var community = CommunityEntity.ServerInstance; +SendInfo sendInfo = new SendInfo(player.net.connection); +community.ClientRPCEx(sendInfo, null, "AddUI", json); +``` + +Destroying UI: +```c# +BasePlayer player = targetPlayer; +string panel = "AddUi CreatedPanel"; // the name of the Panel you wish to destroy +var community = CommunityEntity.ServerInstance; +SendInfo sendInfo = new SendInfo(player.net.connection); +community.ClientRPCEx(sendInfo, null, "DestroyUI", panel); +``` +When destroying a Panel, all child panels will also get destroyed. +> Your Modding Framework may have helper Methods to simplify these steps. + +---- +The next Topic explains Components in detail + +**[Back to the Start](/README.md)** | **[Next Topic](/docs/components/README.md) >** diff --git a/docs/Bugs-Tips.md b/docs/Bugs-Tips.md new file mode 100644 index 0000000..65b56fb --- /dev/null +++ b/docs/Bugs-Tips.md @@ -0,0 +1,133 @@ +# Bugs & Tips + +**< [Previous Topic](/docs/components/README.md)** | **[Back to the Start](/README.md)** + +When working with the CUI System, you will often encounter weird Bugs & Situations. This document lists some ways to prevent them & shows you additional tricks to enhance your UIs. + +## Table of Contents +- [Common Error Messages](#common-error-messages) +- [Bugs](#bugs) +- [Tricks](#tricks) + + +# Common Error Messages +> these errors will appear in your client console when testing the UI, make sure to test for these before releasing your mod + +### AddUI: Unknown Parent for *name*: *parent* +this happens when the parent you supplied in your panel Payload doesn't exist. Double-check for spelling mistakes, and formatting issues & ensure your parent appears in your JSON list before the Panel you get this error on. + + + +--- + +### Error downloading image: *URL* ( *error* ) +the Client couldn't download your Image, double-check your URL & look at the error part within the brackets for what could cause it. + + + +--- + +### A GameObject can only contain one 'Graphic' component. + +This error indicates that you're trying to add multiple Unity Components that derive from `UnityEngine.UI.Graphic`. The **[Components](/docs/components/README.md)** topic has a list of allowed Components and lists the categories they are in. any Component of the **Visual** Category is a Component that derives from or adds a `UnityEngine.UI.Graphic` to the Panel. To get around this Limitation, you can put other Visual Components on a child panel instead. + +# Bugs + + +### Orphaned UI Panels, UI that Can't be Destroyed, or Ghost Panels. + +**Symptom:** you have 1 or more panels that are stuck on your Screen and can't be removed by Calling **DestroyUI** with their Name. + +**Cause:** you have 2 or more panels that shared the same name & were active at the same time. If these are parented directly to a Layer and one is destroyed, the other Panel Can't be destroyed anymore. The only way to remove the UI Panel from your Screen is by reconnecting. Double-check your UI and ensure any Panel directly parented to a Layer has a unique Name. Also, ensure that you don't accidentally send the same UI twice. + + + +--- + + +### Flickering when destroying & re-sending UI on the same frame. + +**Symptom:** when updating Part of your UI. the Player gets a noticeable Delay between the old UI being destroyed & the new UI being created. + +**Cause:** the presumed cause of this issue is the Server Delaying your AddUI Packet to the next Network Cycle. This happens very unpredictably, but is more likely with larger UI Updates. The solution is to use the `destroyUi` field on your panel Payload, it works the same way a DestroyUI call works, but is combined with your panel Payload to prevent network scheduling from creating the flickering issue. + + + +--- + +### Very Inconsistent Alpha handling for images + +**Symptom:** when using images with somewhat transparent pixels, the images get rendered more Opaque than they are, resulting in heavy artifacts. +**Linked Issue:** **[#42 - Rust has an Opacity Issue](https://github.com/Facepunch/Rust.Community/issues/42)** + +**Cause:** no known Cause or Workaround found. Vote on the Linked issue if you encounter this bug or have more info as to why this might happen. + + + +--- + +### Panels with a blur Material & Transparency cause underlying Panels to be invisible. +**Symptom:** when using a blurry Material on a Color with Transparency, any underlying Image Components with blur get cut out. +**Visual Example:** +![image](https://user-images.githubusercontent.com/33698270/215882128-d1f0798c-d7ed-4986-9675-4fba48632ad7.png) +> an image of the following UI setup +> parent: black with 98% alpha +> +> square 1: child of Parent, gray with 20% alpha +> +> square 2: child of Parent, gray with 100% alpha +> +> square 3: child of Parent, gray with 20% alpha and a text element behind it. + + +**Cause:** no exact Cause is known, but it seems to be related to the blurry Materials themselves. This bug only occurs when all underlying Images are using a blurry material with transparency. If at least 1 element is opaque, the bug does not occur. This can be a text or image element. + + + +# Tricks + +### Use Outlines like an advanced User +Outlines can go way beyond the basic use case of putting them on your square Panels. Applying Outlines to Text can be a great way to make them stand out, for example when you can't control the background behind it. +![image](https://user-images.githubusercontent.com/33698270/215885917-4916ee09-a891-4609-82a4-51bb07881bde.png) + +> Can you read this? + +![image](https://user-images.githubusercontent.com/33698270/215886796-346be279-2d8e-43c4-a28f-f740bcf50ff5.png) + +> Adding a 1px outline with a darker color makes it much clearer. + +Another advanced trick is stacking Outlines with lowered Opacity to create a staggered outline or glow/shadow effect. Unlike **Visual** Components, the Outline Component doesn't derive from `UnityEngine.UI.Graphic`, meaning there's no limit on how many Outlines a Panel can have. + +![image](https://user-images.githubusercontent.com/33698270/215899049-e0aa0cd7-b607-466e-a0b7-0cfafa62bdae.png) + +> right has no outlines. left is simulating a 3D effect using 3 stacked Outlines with low opacity. looks great in certain scenarios. + + +--- + + +### Positioning your CUI alongside rust UI + +for features that augment Rust's existing UI, like when you're working with Furnaces & other storage inventories, it's important to make sure your CUI doesn't interfere with the UI you augment. + +Rust UI uses the same Placement system CUI does. It primarily uses offsets for sizing & positioning and anchors to a specific Point depending on the UI. by doing the same, you can ensure that your CUI is positioned properly, regardless of Screen size & Aspect Ratio. + +UIs like the inventory, belt bar & storage containers are anchored to the bottom of the screen, but many other UIs don't follow that convention. + +A great way to discover what a UI element is anchored to is to use the windowed mode and stretch it to extreme aspect ratios. This clearly reveals anchoring thanks to rust's offset scaling +![image](https://user-images.githubusercontent.com/33698270/216077347-5461623c-8ff4-4890-8633-062519c4e371.png) +> Stretching our window show that the Crafting & Contacts buttons are anchored to the top middle, while the status bars are anchored to the bottom right corner + +a RectTransform Example that covers the Belt-bar +```json +{ + "type": "RectTransform", + "anchormin": "0.5 0.0", + "anchormax": "0.5 0.0", + "offsetmin": "-200 18", + "offsetmax": "180 78" +} +``` +![image](https://user-images.githubusercontent.com/33698270/215901408-c152fecb-8453-4597-8cd6-61038b2b976d.png) + +**< [Previous Topic](/docs/components/README.md)** | **[Back to the Start](/README.md)** diff --git a/docs/components/Countdown.md b/docs/components/Countdown.md new file mode 100644 index 0000000..75969ca --- /dev/null +++ b/docs/components/Countdown.md @@ -0,0 +1,40 @@ +# Components: Countdown +**< [Previous Component](/docs/components/NeedsX.md)** | **[Back to Components](/docs/components/README.md)** + +- Identifier: `Countdown` +- Category: **Misc** +- Unity Documentation: **N/A** + +The Countdown Component lets you turn an existing Text Component into a Timer, Countdown, or Count-up. The Countdown Component automatically **destroys** the Panel when it’s done counting. +```json +{ + "type": "Countdown", + "endtime": 0, + "startTime": 0, + "step": 1, + "command": "" +} +``` + +Countdown-specific Fields: +| Key | Type | Notes | +| :---------- | :----- | :------------------- | +| `endTime` | int | The Number it should count to | +| `startTime` | int | The Number it should start counting from | +| `step` | int | The Number it should increment by, also acts as the update interval | +| `command` | string | The command that should be executed when the Countdown finishes. | + +## Working with Countdowns + +for Countdowns to work, you need to add a **[Text](https://stackedit.io/docs/components/UnityEngine.UI.Text.md)** Component to your Panel. Your Text Component’s content should include the string `%TIME_LEFT%`, which the Countdown will replace with the current Number. + +Examples: +- `the Event will end in %TIME_LEFT% Seconds` ⇒ the Event Will end in 30 Seconds +- `Power-up Buff - %TIME_LEFT%s` ⇒ Power-up Buff - 30s + +### Counting Up vs counting Down +- to count Down, set your `startTime` higher than your `endTime` & set your `step` to at least 1. +- To count Up, set your `startTime` lower than your `endTime` + + +**< [Previous Component](/docs/components/NeedsX.md)** | **[Back to Components](/docs/components/README.md)** diff --git a/docs/components/NeedsX.md b/docs/components/NeedsX.md new file mode 100644 index 0000000..ed4b882 --- /dev/null +++ b/docs/components/NeedsX.md @@ -0,0 +1,19 @@ +# Components: NeedsCursor & NeedsKeyboard +**< [Previous Component](/docs/components/UnityEngine.UI.InputField.md)** | **[Back to Components](/docs/components/README.md)** | **[Next Component](/docs/components/Countdown.md) >** + +- Identifier: `NeedsCursor` or `NeedsKeyboard` +- Category: **Misc** +- Unity Documentation: **N/A** + +The NeedsCursor & NeedsKeyboard Components are Components with no additional Fields. Their only purpose is to tell Rust's input Controller if mouse & keyboard Behavior should only be focused on your UI. +```json +{ + "type": "NeedsCursor" + // or + "type": "NeedsKeyboard" +} +``` + +Unlike the `needsKeyboard` and `hudMenuInput` fields on an InputField Component, these Components Prevent default Behavior until your Panel is Destroyed + +**< [Previous Component](/docs/components/UnityEngine.UI.InputField.md)** | **[Back to Components](/docs/components/README.md)** | **[Next Component](/docs/components/Countdown.md) >** diff --git a/docs/components/README.md b/docs/components/README.md new file mode 100644 index 0000000..fb95435 --- /dev/null +++ b/docs/components/README.md @@ -0,0 +1,32 @@ +# Components +**< [Previous Topic](/docs/Basics.md)** | **[Back to the Start](/README.md)** | **[Next Topic](/docs/Bugs-Tips.md) >** + +The CUI System comes with a Set of Components you can use to build your UI, some are visual, and others add Interactivity. + +Components are added to a panel by sending them as a List in the Schema shown in [The Basics](/docs/Basics.md). To identify the Type of Component sent, a `type` Field is added by every Component. +```json +[{ + "type": "UnityEngine.UI.Text", + // More Component fields ... +}, +...] +``` + +## Component List +- [RectTransform](/docs/components/RectTransform.md) +- [RawImage](/docs/components/UnityEngine.UI.RawImage.md) +- [Image](/docs/components/UnityEngine.UI.Image.md) +- [Text](/docs/components/UnityEngine.UI.Text.md) +- [Outline](/docs/components/UnityEngine.UI.Outline.md) +- [Button](/docs/components/UnityEngine.UI.Button.md) +- [InputField](/docs/components/UnityEngine.UI.InputField.md) +- [NeedsCursor & NeedsKeyboard](/docs/components/NeedsX.md) +- [Countdown](/docs/components/Countdown.md) + +## About Component Categories +Each Component Page has a _Category_ listed, which helps you identify what a Component does. Pay attention to the **Visual** Category, as it means the Component Adds a Unity Component deriving from `UnityEngine.UI.Graphic`. This is important because **each panel can only have 1 Graphic Component**. + +--- +The next Topic explains common Pitfalls & Things you need to look out for + +**< [Previous Topic](/docs/Basics.md)** | **[Back to the Start](/README.md)** | **[Next Topic](/docs/Bugs-Tips.md) >** diff --git a/docs/components/RectTransform.md b/docs/components/RectTransform.md new file mode 100644 index 0000000..78fa0f2 --- /dev/null +++ b/docs/components/RectTransform.md @@ -0,0 +1,63 @@ +# Components: RectTransform +**[Back to Components](/docs/components/README.md)** | **[Next Component](/docs/components/UnityEngine.UI.RawImage.md) >** + +- Identifier: `RectTransform` +- Category: **Position** +- Unity Documentation: **[RectTransform @ docs.unity3d.com](https://docs.unity3d.com/Packages/com.unity.ugui@1.0/manual/class-RectTransform.html)** + +The RectTransform is a Component that specifies the Position & Size of the Panel it's attached to. +It allows you to position a Panel via [Anchors & Offsets](#anchors-and-offsets). +```json +{ + "type": "RectTransform", + "anchormin": "0.0 0.0", + "anchormax": "1.0 1.0", + "offsetmin": "0.0 0.0", + "offsetmax": "1.0 1.0", +} +``` +RectTransform specific Fields: +| Key | Type | Notes | +| :---------- | :----- | :------------------- | +| `anchormin` | string | Specifies the anchor for the bottom left Corner | +| `anchormax` | string | Specifies the anchor for the top right Corner | +| `offsetmin` | string | Specifies the Pixel offset for the bottom left Corner | +| `offsetmax` | string | Specifies the Pixel offset for the top right Corner | + +both Anchors & Offsets come with a *-min* and *-max* Field, where the min holds x & y Values for the bottom left Corner & the max holds x & y Values for the top right Corner + +## Positioning in Rust +a Panel's Position is always relative to its Parent, even when parenting directly to a Layer. +Unity's Coordinate System in a 2D Space goes from Left to Right, Bottom to Top. +| `x 0.0 y 1.0` | `x 1.0 y 1.0` | +| ------------- | ------------- | +| `x 0.0 y 0.0` | `x 1.0 y 0.0` | +> this might be confusing to people coming from the web-design space, where the x coordinate commonly goes down the screen instead of up + +## Anchors and Offsets +Anchors Specify where your Panel starts & ends. Offsets let you move the start & end points with pixel values. + +Anchors use a normalized Percentage of the Parent, where `0.0` is the Start and `1.0` is the End. [The “Anchors” Section](https://docs.unity3d.com/Packages/com.unity.ugui@1.0/manual/UIBasicLayout.html#anchors) in the Unity Documentation has some visual examples which also apply here. + +Offsets use [scaled](#offsets-and-scaling) pixel values to move the corners relative to the Anchors you provide. + +Positioning using only anchors is great for when you want your Panel to cover a Percentage of the Screen or Parent, but for any other cases, a mixture of Anchors & Offsets should be used to position your UI. + +A common Technique is to use anchors to specify what part of the Parent the UI should be anchored to (Top Left Corner or Dead Center. Etc.) and to use Offsets for Size & Position. +Anchors Specify where your Panel starts & ends. Offsets let you move the start & end Points with pixel Values. + +## Offsets and Scaling + +Rust will automatically scale offsets depending on the Player’s Resolution, which ensures your Offsets are predictable. + +By default, Rust handles Offsets based on a Resolution of **1280** by **720**. This means that for smaller & larger **16:9** Resolutions, Rust will simply multiply Offset by the difference between the Player’s Resolution and 720p, + +For non-16:9 Resolutions such as Ultrawide or windowed Mode, Rust will apply Letter boxing before calculating the scaling Factor + +> NOTE: when scaling offsets rust also takes the player's `graphics.uiscale` into account. + +https://user-images.githubusercontent.com/33698270/214582004-c695e73c-9125-4ccb-9cfb-abc883b5c0a0.mp4 +> a video of a gray Rectangle positioned at the Center of the Screen (anchor min & max at `0.5 0.5`) with an offset of 1280 x 720 px. see how Unity applies Letter boxing before scaling the Rectangle to ensure it never gets stretched. NOTE: the black Bars are only to illustrate in this Example, your in game UI won’t have any black Bars + + +**[Back to Components](/docs/components/README.md)** | **[Next Component](/docs/components/UnityEngine.UI.RawImage.md) >** diff --git a/docs/components/UnityEngine.UI.Button.md b/docs/components/UnityEngine.UI.Button.md new file mode 100644 index 0000000..7799728 --- /dev/null +++ b/docs/components/UnityEngine.UI.Button.md @@ -0,0 +1,36 @@ +# Components: Button +**< [Previous Component](/docs/components/UnityEngine.UI.Outline.md)** | **[Back to Components](/docs/components/README.md)** | **[Next Component](/docs/components/UnityEngine.UI.InputField.md) >** + +- Identifier: `UnityEngine.UI.Button` +- Category: **Interactive, Visual** +- Unity Documentation: **[Button @ docs.unity3d.com](https://docs.unity3d.com/Packages/com.unity.ugui@1.0/manual/script-Button.html)** + +The Button Component is an Interactive Component that lets you execute Commands & Destroy UI when it gets pressed. It automatically adds a `UnityEngine.UI.Image` to the panel, allowing you to change the `sprite`, `material` & `color` on it. +```json +{ + "type": "UnityEngine.UI.Button", + "command": "", + "close": "", + "sprite": "Assets/Icons/rust.png", + "color": "1.0 1.0 1.0 1.0", + "material": "", + "imagetype": "Simple", + "fadeIn": 0.0 +} +``` +Button-specific Fields: +| Key | Type | Notes | +| :---------- | :----- | :------------------- | +| `command` | string | The command that should get sent to the Server on Click | +| `close` | string | The Name of the Panel that should get closed on Click | +| `sprite` | string | The asset Path to the sprite | +| `color` | string | The normalized RGBA values of your color | +| `material` | string | The asset Path to the Material | +| `imagetype` | string (enum `Image.Type`) | Sets the display mode of the Image* | +| `fadeIn` | float | The Duration the Panel should take to fade in | +\* Currently non-functioning for anything other than Rust's built-in Sprites + +### Button as a Parent +click events bubble up, meaning that they will get triggered on every Panel until a Component consumes them. This means it’s possible to use the Button as a Parent and still get notified when the Player clicks a child panel. + +**< [Previous Component](/docs/components/UnityEngine.UI.Outline.md)** | **[Back to Components](/docs/components/README.md)** | **[Next Component](/docs/components/UnityEngine.UI.InputField.md) >** diff --git a/docs/components/UnityEngine.UI.Image.md b/docs/components/UnityEngine.UI.Image.md new file mode 100644 index 0000000..b34ef0e --- /dev/null +++ b/docs/components/UnityEngine.UI.Image.md @@ -0,0 +1,68 @@ +# Components: Image +**< [Previous Component](/docs/components/UnityEngine.UI.RawImage.md)** | **[Back to Components](/docs/components/README.md)** | **[Next Component](/docs/components/UnityEngine.UI.Text.md) >** + +- Identifier: `UnityEngine.UI.Image` +- Category: **Visual** +- Unity Documentation: **[Image @ docs.unity3d.com](https://docs.unity3d.com/Packages/com.unity.ugui@1.0/manual/script-Image.html)** + +The Image is a Visual Component that allows you to display images from your Server & the Web or Rust Sprites. It can also be used with a single color to act as a Background for your panel. +```json +{ + "type": "UnityEngine.UI.Image", + "sprite": "Assets/Icons/rust.png", + "color": "1.0 1.0 1.0 1.0", + "material": "", + "imagetype": "Simple", + "png": "", + "itemid": 0, + "skinid": 0, + "fadeIn": 0.0 +} +``` +RawImage specific Fields: +| Key | Type | Notes | +| :---------- | :----- | :------------------- | +| `sprite` | string | The asset Path to the sprite | +| `color` | string | The normalized RGBA values of your color | +| `material` | string | The asset Path to the Material | +| `imagetype` | string (enum `Image.Type`) | Sets the display mode of the Image* | +| `png` | string | The CRC Checksum of the Image hosted on the Server | +| `itemid` | int | The Item ID of your item | +| `skinid` | ulong | The Skin ID of your skin | +| `fadeIn` | float | The Duration the Panel should take to fade in | +\* Currently non-functioning for anything other than Rust's built-in Sprites + +## imagetype and sprites +the imagetype lets you provide a specific `Image.Type` option to change how the Component renders the image. + +An important thing to remember is that currently, Simple is the only option that works with images loaded from your server. This is because the Sliced, Tiled & Filled options require extra parameters to be set on the sprite. The image Component's JSON API currently doesn't wrap these parameters. + +Available options for `Image.Type` +| Value | Description | +| :---- | :---------- | +| `Simple` | Displays the full Image | +| `Sliced` | Displays the Image as a 9-sliced graphic. | +| `Tiled` | Displays a sliced Sprite with its resizable sections tiled instead of stretched. | +| `Filled` | Displays only a portion of the Image. | + +## RawImages vs Images +Like RawImages, Images share the ability to show Sprites, Colors, Materials & Images hosted on the Server, but they cannot directly load images from the Web. + +The Image Component has convenient Ways to display any Item or Skin and is recommended when using Rust's Sprites. + +## Items and Skins +using the `itemid` & `skinid` fields, you can let the Client handle the displaying of related Images. +NOTE: there is currently a bug when supplying a skinid of `0`, causing the player to crash, avoid including the field altogether if you don't intend to send a skinid + +Tip: use the ItemDefinition of your Item to easily find an Item’s ID & other useful Information +```c# +string shortname = "your.item.shortname"; +var itemDef = ItemManager.FindItemDefinition(shortname); +int itemid = itemDef.itemid; +// other useful Fields: displayName, displayDescription, category & stackable +``` + + +--- +check the [RawImage](/docs/components/UnityEngine.UI.RawImage.md) Documentation to learn about Fields the Components share. +**< [Previous Component](/docs/components/UnityEngine.UI.RawImage.md)** | **[Back to Components](/docs/components/README.md)** | **[Next Component](/docs/components/UnityEngine.UI.Text.md) >** diff --git a/docs/components/UnityEngine.UI.InputField.md b/docs/components/UnityEngine.UI.InputField.md new file mode 100644 index 0000000..ac4f77d --- /dev/null +++ b/docs/components/UnityEngine.UI.InputField.md @@ -0,0 +1,58 @@ +# Components: InputField +**< [Previous Component](/docs/components/UnityEngine.UI.Button.md)** | **[Back to Components](/docs/components/README.md)** | **[Next Component](/docs/components/NeedsX.md) >** + +- Identifier: `UnityEngine.UI.InputField` +- Category: **Interactive, Visual** +- Unity Documentation: **[InputField @ docs.unity3d.com](https://docs.unity3d.com/Packages/com.unity.ugui@1.0/manual/script-InputField.html)** + +The InputField Component is an Interactive Component that allows Players to enter arbitrary text that gets sent back as a Command. It automatically adds a `UnityEngine.UI.Text` to the panel, allowing you to change the `fontSize`, `font`, `align`ment & text`color` of it. +```json +{ + "type": "UnityEngine.UI.InputField", + "fontSize": 14, + "font": "RobotoCondensed-Bold.ttf", + "align": "UpperLeft", + "color": "1.0 1.0 1.0 1.0", + "command": "", + "text": "", + "readOnly": false, + "lineType": "SingleLine", + "password": null, + "needsKeyboard": null, + "hudMenuInput": null, + "autofocus": null, + "fadeIn": 0.0 +} +``` +> `password`, `needsKeyboard`, `hudMenuInput`, and `autofocus` are key presence Fields, key presence Fields don't have a specific type and act as a Boolean. +> If the key is present it equals true, if absent it equals false. + +InputField specific Fields: +| Key | Type | Notes | +| :---------- | :----- | :------------------- | +| `command` | string | The command that should get sent to the Server alongside the Player's Input. The Input will get appended to the Command after a Space. | +| `text` | string | The default Text of the InputField can be combined with `readOnly` | +| `readOnly` | bool | Prevents the Content from being edited | +| `lineType` | string (enum `InputField.LineType`) | Dictates if the Field should allow multiple Lines & how to handle when the Player presses `enter` | +| `password` | key presence Field | If the input should be obscured | +| `needsKeyboard` | key presence Field | Prevents default Keyboard behavior (movement, item switching etc.) While the field is Focused | +| `hudMenuInput` | key presence Field | Same as above but for Rust UI (Inventory, Crafting, etc.) | +| `autofocus` | key presence Field | Selects the field upon creation | +| `fadeIn` | float | The Duration the Panel should take to fade in | + +### needsKeyboard vs hudMenuInput +while both prevent Vanilla behavior, they have some key differences that are good to keep in mind. +needsKeyboard works well for normal use, but will close any Rust UI the player has open when the player selects an InputField with it enabled. +This is the primary reason why hudMenuInput was added. It won't close Rust's UI when selected, but won't prevent the player from moving & executing key binds. + +### Selecting Text +an underutilized Power of the InputField is that you can select its contents. This is helpful when creating forms & editors, but can also be used for other features. Like using it for displaying links to your website or discord, allowing players to select and copy it instead of having to type it out. + +It’s recommended to wrap your InputField in another panel, ensuring it's the only child of it’s parent, as it prevents the selected text from being covered by other children. + +### Receiving Input & the lineType Setting +to receive the Player’s input text, listen for the command you specify in the `command` field. The Input will get sent as soon as the Player unfocuses the InputField, for example by clicking out of it. + +Depending on the `lineType` Setting, if it’s set to SingleLine or MultiLineSubmit pressing `enter` will also cause the Input to get sent to the Server. Pressing `enter` with the MultiLineNewline Setting inserts a Newline instead. + +**< [Previous Component](/docs/components/UnityEngine.UI.Button.md)** | **[Back to Components](/docs/components/README.md)** | **[Next Component](/docs/components/NeedsX.md) >** diff --git a/docs/components/UnityEngine.UI.Outline.md b/docs/components/UnityEngine.UI.Outline.md new file mode 100644 index 0000000..e59c8da --- /dev/null +++ b/docs/components/UnityEngine.UI.Outline.md @@ -0,0 +1,27 @@ +# Components: Outline +**< [Previous Component](/docs/components/UnityEngine.UI.Text.md)** | **[Back to Components](/docs/components/README.md)** | **[Next Component](/docs/components/UnityEngine.UI.Button.md) >** + +- Identifier: `UnityEngine.UI.Outline` +- Category: **Effect** +- Unity Documentation: **[Outline @ docs.unity3d.com](https://docs.unity3d.com/Packages/com.unity.ugui@1.0/manual/script-Outline.html)** + +The Outline Component is an Effect Component that puts a colored outline on any Visual Component. It supports any Image, Text, Button, or Input field. +```json +{ + "type": "UnityEngine.UI.Outline", + "color": "1.0 1.0 1.0 1.0", + "distance": "1.0 -1.0", + "useGraphicAlpha": null +} +``` +> `useGraphicAlpha` is a key presence Field, key presence Fields don't have a specific type and act as a Boolean. +> If the key is present it equals true, if absent it equals false. + +Outline specific Fields: +| Key | Type | Notes | +| :---------- | :----- | :------------------- | +| `color` | string | The Color of your Outline | +| `distance` | string | The distance of your Outline (formatted as `X Y`) | +| `useGraphicAlpha` | key presence Field | Multiplies the Alpha of the graphic onto the color of the Outline | + +**< [Previous Component](/docs/components/UnityEngine.UI.Text.md)** | **[Back to Components](/docs/components/README.md)** | **[Next Component](/docs/components/UnityEngine.UI.Button.md) >** diff --git a/docs/components/UnityEngine.UI.RawImage.md b/docs/components/UnityEngine.UI.RawImage.md new file mode 100644 index 0000000..47ea6d7 --- /dev/null +++ b/docs/components/UnityEngine.UI.RawImage.md @@ -0,0 +1,97 @@ +# Components: RawImage +**< [Previous Component](/docs/components/RectTransform.md)** | **[Back to Components](/docs/components/README.md)** | **[Next Component](/docs/components/UnityEngine.UI.Image.md) >** + +- Identifier: `UnityEngine.UI.RawImage` +- Category: **Visual** +- Unity Documentation: **[RawImage @ docs.unity3d.com](https://docs.unity3d.com/Packages/com.unity.ugui@1.0/manual/script-RawImage.html)** + +the most common use for RawImage is its Ability to display any* Color you give it. Therefore, it’s often used to give panels a Background Color. Colors are sent as a `string` of 4 normalized `floats` + +Tip: if you’re used to using hexadecimal, you can use this function to convert your colors on the fly: +```json +{ + "type": "UnityEngine.UI.RawImage", + "sprite": "Assets/Icons/rust.png", + "color": "1.0 1.0 1.0 1.0", + "material": "", + "url": "", + "png": "", + "fadeIn": 0.0 +} +``` +RawImage specific Fields: +| Key | Type | Notes | +| :---------- | :----- | :------------------- | +| `sprite` | string | The asset Path to the sprite | +| `color` | string | The normalized RGBA values of your color | +| `material` | string | The asset Path to the Material | +| `url` | string | The URL of the Image you want to show | +| `png` | string | The CRC Checksum of the Image hosted on the Server | +| `fadeIn` | float | The Duration the Panel should take to fade in | + +## Images can be your Backgrounds +the most common use for RawImage is its Ability to display any* Color you give it. therefore it's often used to give Panels a Background Color. Colors are sent as a `string` of 4 normalized `floats` + +Tip: if you're used to using hexadecimal you can use this function to convert your colors on the fly: +```c# +static public string NormalizeHex(string hex, byte alpha = 255){ + if (string.IsNullOrEmpty(hex) || hex.Length < 7) + hex = "#FFFFFF"; + var str = hex.Trim('#'); + + var r = byte.Parse(str.Substring(0, 2), System.Globalization.NumberStyles.HexNumber); + var g = byte.Parse(str.Substring(2, 2), System.Globalization.NumberStyles.HexNumber); + var b = byte.Parse(str.Substring(4, 2), System.Globalization.NumberStyles.HexNumber); + var a = alpha; + // if the hex has alpha encoded, use it instead + if (str.Length >= 8) + a = byte.Parse(str.Substring(6, 2), System.Globalization.NumberStyles.HexNumber); + + return $"{(double)r / 255:f3} {(double)g / 255:f3} {(double)b / 255:f3} {(double)a / 255:f3}"; +} +``` + +Images can also be combined with a Subset of Materials to enhance your UIs, some Materials can be combined with a Color, while others will enforce their own Color. some Materials won't have any visual Effect. +> \* Colors are limited to a Precision of 0.01 per Channel, giving you only 1,000,000 Colors to choose from (excluding the alpha Channel) + +## Using Rust's Sprites +You can re-use any of Rust's Sprites by specifying an asset path in the `sprite` field. + +## Loading your own Images +the RawImage Component has two Ways of displaying your own Images, loading from the server & loading from the web. + +Loading images from the server is done by downloading the image onto the game Server & supplying the CRC Checksum in the `png` field. + +Loading images from the web is done by hosting the image online and supplying a direct link to it in the `url` field + +Loading Images from your Server requires a bit of setup but has the benefit of the Client caching the image once it’s loaded from your server, resulting in faster loading times on subsequent UI sends. + +### Loading Images from your Server +to load images from your own Server, you have to download & store the image in Rust's `FileStorage`. + +```c# + +private IEnumerator SaveImageFromURL(string URL){ + using (UnityWebRequest request = UnityWebRequestTexture.GetTexture(URL)){ + // Download your image from the web + yield return request.SendWebRequest(); + + if (uwr.result != UnityWebRequest.Result.Success) + yield break; + + var texture = DownloadHandlerTexture.GetContent(request); + byte[] data = texture.EncodeToPNG(); + + // this CRC is what you send to the client. save it somewhere for later + uint crc = FileStorage.server.Store(data, FileStorage.Type.png, CommunityEntity.ServerInstance.net.ID); + } +} +``` +The rust Client will then automatically fetch the Image from the Server. You will have to re-download the image every Wipe as the CommunityEntity’s netID will differ, which will cause the Client to not find your Image. + +## Combining Images & Color +when using an image you can use the `color` property to modify the image's Color on the fly, this is great when using Icons that are White or Grayscale, as it allows you to communicate Information by changing the Color of the Icon instead of having to load multiple versions. + +--- +Before using RawImage, take a look at the Image Component to see if it’s better suited for your use case. +**< [Previous Component](/docs/components/README.md)** | **[Back to Components](/docs/components/README.md)** | **[Next Component](/docs/components/UnityEngine.UI.Image.md) >** diff --git a/docs/components/UnityEngine.UI.Text.md b/docs/components/UnityEngine.UI.Text.md new file mode 100644 index 0000000..fe72820 --- /dev/null +++ b/docs/components/UnityEngine.UI.Text.md @@ -0,0 +1,69 @@ +# Components: Text +**< [Previous Component](/docs/components/UnityEngine.UI.Image.md)** | **[Back to Components](/docs/components/README.md)** | **[Next Component](/docs/components/UnityEngine.UI.Outline.md) >** + +- Identifier: `UnityEngine.UI.Text` +- Category: **Visual** +- Unity Documentation: **[Text @ docs.unity3d.com](https://docs.unity3d.com/Packages/com.unity.ugui@1.0/manual/script-Text.html)** + +The Text Component is a Visual Component that allows you to display any Text you want. It has Options for Alignment, Color, and overflow Options & Supports RichText. +```json +{ + "type": "UnityEngine.UI.Text", + "text": "Text", + "fontSize": 14, + "font": "RobotoCondensed-Bold.ttf", + "align": "UpperLeft", + "color": "1.0 1.0 1.0 1.0", + "verticalOverflow": "Truncate", + "fadeIn": 0.0 +} +``` +Text-specific Fields: +| Key | Type | Notes | +| :---------- | :----- | :------------------- | +| `text` | string | The Content of your Text Component | +| `fontSize` | int | The default font size of your Text | +| `font` | string | The Asset name of the Font you wish to use | +| `align` | string (enum `TextAnchor`) | The way your Text should be aligned | +| `color` | string | The default Color of your Text | +| `verticalOverflow` | string (enum `VerticalWrapMode`) | How Text Overflowing vertically should be handled | +| `fadeIn` | float | The Duration the Panel should take to fade in | + +### Available Fonts: +- `DroidSansMono.ttf` +- `PermanentMarker.ttf` +- `RobotoCondensed-Bold.ttf` +- `RobotoCondensed-Regular.ttf` + +### Text Alignment Options: +The TextAnchor enum has the following options available +| `UpperLeft` | `UpperCenter` | `UpperRight` | +| ------------ | -------------- | ------------- | +| `MiddleLeft` | `MiddleCenter` | `MiddleRight` | +| `LowerLeft` | `LowerCenter` | `LowerRight` | + +### VerticalWrapMode Options: +The VerticalWrapMode enum has the following options available +| Option | Notes | +| :----- | :---- | +| 'Truncate'| Text will be clipped when reaching the bottom. | +| 'Overflow'| Text will continue to generate past the bottom edge of the panel. | + +## Using Unity RichText +Rust Allows you to use Unity **[Rich Text](https://docs.unity3d.com/2021.3/Documentation/Manual/StyledText.html)** to give you more control over text styling. This lets you empathize Paragraphs, Sentences, Words & individual Letters. + +To modify a selection of text, simply wrap it in XML-style tags: `i am bold` +RichText tags can be Nested to combine Effects: `bold and in red` + +Supported Tags: +| **_Tag_** | **_Description_** | **_Example_** | +| :-------: | :----------------- | :------------ | +| **b** | Renders the Text as bold | `I am bold` | +| **i** | Renders the Text as italic | `I am italic` | +| **size** | Sets the fontSize to the Value supplied | `I am small. i am not.` | +| **color** | Changes the Color of the Text | `I am Coffee coloured` | +For colors, Unity Supports an assortment of named Colors you can find in **[this table](https://docs.unity3d.com/2021.3/Documentation/Manual/StyledText.html#ColorNames).** + + + +**< [Previous Component](/docs/components/UnityEngine.UI.Image.md)** | **[Back to Components](/docs/components/README.md)** | **[Next Component](/docs/components/UnityEngine.UI.Outline.md) >**