using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Net.Http; using System.Text; using System.Text.RegularExpressions; using System.Threading.Tasks; using Edgegap.Editor.Api; using Edgegap.Editor.Api.Models; using Edgegap.Editor.Api.Models.Requests; using Edgegap.Editor.Api.Models.Results; using UnityEditor; using UnityEditor.Build.Reporting; using UnityEditor.UIElements; using UnityEngine; using UnityEngine.Assertions; using UnityEngine.UIElements; using Application = UnityEngine.Application; namespace Edgegap.Editor { /// <summary> /// Editor logic event handler for "UI Builder" EdgegapWindow.uxml, superceding` EdgegapWindow.cs`. /// </summary> public class EdgegapWindowV2 : EditorWindow { #region Vars public static bool IsLogLevelDebug => EdgegapWindowMetadata.LOG_LEVEL == EdgegapWindowMetadata.LogLevel.Debug; private bool IsInitd; private VisualTreeAsset _visualTree; private bool _isApiTokenVerified; // Toggles the rest of the UI private bool _isContainerRegistryReady; private Sprite _appIconSpriteObj; private string _appIconBase64Str; #pragma warning disable CS0414 // MIRROR CHANGE: hide unused warning private ApiEnvironment _apiEnvironment; // TODO: Swap out hard-coding with UI element? #pragma warning restore CS0414 // END MIRROR CHANGE private GetRegistryCredentialsResult _credentials; private static readonly Regex _appNameAllowedCharsRegex = new Regex(@"^[a-zA-Z0-9_\-+\.]*$"); // MIRROR CHANGE: 'new()' not supported in Unity 2020 private GetCreateAppResult _loadedApp; /// <summary>TODO: Make this a list</summary> private GetDeploymentStatusResult _lastKnownDeployment; private string _deploymentRequestId; private string _userExternalIp; private bool _isAwaitingDeploymentReadyStatus; #endregion // Vars #region Vars -> Interactable Elements private Button _debugBtn; /// <summary>(!) This is saved manually to EditorPrefs via Base64 instead of via UiBuilder</summary> private TextField _apiTokenInput; private Button _apiTokenVerifyBtn; private Button _apiTokenGetBtn; private VisualElement _postAuthContainer; private Foldout _appInfoFoldout; private Button _appLoadExistingBtn; private TextField _appNameInput; /// <summary>`Sprite` type</summary> private ObjectField _appIconSpriteObjInput; private Button _appCreateBtn; private Label _appCreateResultLabel; private Foldout _containerRegistryFoldout; private TextField _containerNewTagVersionInput; private TextField _containerPortNumInput; // MIRROR CHANGE: EnumField Port type fails to resolve unless in Assembly-CSharp-Editor.dll. replace with regular Dropdown instead. /// <summary>`ProtocolType` type</summary> // private EnumField _containerTransportTypeEnumInput; private PopupField<string> _containerTransportTypeEnumInput; // END MIRROR CHANGE private Toggle _containerUseCustomRegistryToggle; private VisualElement _containerCustomRegistryWrapper; private TextField _containerRegistryUrlInput; private TextField _containerImageRepositoryInput; private TextField _containerUsernameInput; private TextField _containerTokenInput; private Button _containerBuildAndPushServerBtn; private Label _containerBuildAndPushResultLabel; private Foldout _deploymentsFoldout; private Button _deploymentsRefreshBtn; private Button _deploymentsCreateBtn; /// <summary>display:none (since it's on its own line), rather than !visible.</summary> private Label _deploymentsStatusLabel; private VisualElement _deploymentsServerDataContainer; private Button _deploymentConnectionCopyUrlBtn; private TextField _deploymentsConnectionUrlReadonlyInput; private Label _deploymentsConnectionStatusLabel; private Button _deploymentsConnectionStopBtn; private Button _footerDocumentationBtn; private Button _footerNeedMoreGameServersBtn; #endregion // Vars // MIRROR CHANGE // get the path of this .cs file so we don't need to hardcode paths to // the .uxml and .uss files: // https://forum.unity.com/threads/too-many-hard-coded-paths-in-the-templates-and-documentation.728138/ // this way users can move this folder without breaking UIToolkit paths. internal string StylesheetPath => Path.GetDirectoryName(AssetDatabase.GetAssetPath(MonoScript.FromScriptableObject(this))); // END MIRROR CHANGE // MIRROR CHANGE: images are dragged into the script in inspector and assigned to the UI at runtime. this way we don't need to hardcode it. public Texture2D LogoImage; public Texture2D ClipboardImage; // END MIRROR CHANGE [MenuItem("Edgegap/Edgegap Hosting")] // MIRROR CHANGE: more obvious title public static void ShowEdgegapToolWindow() { EdgegapWindowV2 window = GetWindow<EdgegapWindowV2>(); window.titleContent = new GUIContent("Edgegap Hosting"); // MIRROR CHANGE: 'Edgegap Server Management' is too long for the tab space window.maxSize = new Vector2(635, 900); window.minSize = window.maxSize; } #region Unity Funcs protected void OnEnable() { #if UNITY_2021_3_OR_NEWER // MIRROR CHANGE: only load stylesheet in supported Unity versions, otherwise it shows errors in U2020 // Set root VisualElement and style: V2 still uses EdgegapWindow.[uxml|uss] // BEGIN MIRROR CHANGE _visualTree = AssetDatabase.LoadAssetAtPath<VisualTreeAsset>($"{StylesheetPath}/EdgegapWindow.uxml"); StyleSheet styleSheet = AssetDatabase.LoadAssetAtPath<StyleSheet>($"{StylesheetPath}/EdgegapWindow.uss"); // END MIRROR CHANGE rootVisualElement.styleSheets.Add(styleSheet); #endif } #pragma warning disable CS1998 // MIRROR CHANGE: disable async warning in U2020 public async void CreateGUI() #pragma warning restore CS1998 // END MIRROR CHANGE { // MIRROR CHANGE: the UI requires 'GroupBox', which is not available in Unity 2019/2020. // showing it will break all of Unity's Editor UIs, not just this one. // instead, show a warning that the Edgegap plugin only works on Unity 2021+ #if !UNITY_2021_3_OR_NEWER Debug.LogWarning("The Edgegap Hosting plugin requires UIToolkit in Unity 2021.3 or newer. Please upgrade your Unity version to use this."); #else // Get UI elements from UI Builder rootVisualElement.Clear(); _visualTree.CloneTree(rootVisualElement); // Register callbacks and sync UI builder elements to fields here InitUIElements(); syncFormWithObjectStatic(); await syncFormWithObjectDynamicAsync(); // API calls IsInitd = true; #endif } /// <summary>The user closed the window. Save the data.</summary> protected void OnDisable() { #if UNITY_2021_3_OR_NEWER // MIRROR CHANGE: only load stylesheet in supported Unity versions, otherwise it shows errors in U2020 // MIRROR CHANGE: sometimes this is called without having been registered, throwing NRE if (_debugBtn == null) return; // END MIRROR CHANGE unregisterClickEvents(); unregisterFieldCallbacks(); SyncObjectWithForm(); #endif } #endregion // Unity Funcs #region Init /// <summary> /// Binds the form inputs to the associated variables and initializes the inputs as required. /// Requires the VisualElements to be loaded before this call. Otherwise, the elements cannot be found. /// </summary> private void InitUIElements() { setVisualElementsToFields(); assertVisualElementKeys(); closeDisableGroups(); registerClickCallbacks(); registerFieldCallbacks(); initToggleDynamicUi(); AssignImages(); // MIRROR CHANGE } private void closeDisableGroups() { _appInfoFoldout.value = false; _containerRegistryFoldout.value = false; _deploymentsFoldout.value = false; _appInfoFoldout.SetEnabled(false); _containerRegistryFoldout.SetEnabled(false); _deploymentsFoldout.SetEnabled(false); } // MIRROR CHANGE: assign images to the UI at runtime instead of hardcoding it void AssignImages() { // header logo VisualElement logoElement = rootVisualElement.Q<VisualElement>("header-logo-img"); logoElement.style.backgroundImage = LogoImage; // clipboard button VisualElement copyElement = rootVisualElement.Q<VisualElement>("DeploymentConnectionCopyUrlBtn"); copyElement.style.backgroundImage = ClipboardImage; } // END MIRROR CHANGE /// <summary>Set fields referencing UI Builder's fields. In order of appearance from top-to-bottom.</summary> private void setVisualElementsToFields() { _debugBtn = rootVisualElement.Q<Button>(EdgegapWindowMetadata.DEBUG_BTN_ID); _apiTokenInput = rootVisualElement.Q<TextField>(EdgegapWindowMetadata.API_TOKEN_TXT_ID); _apiTokenVerifyBtn = rootVisualElement.Q<Button>(EdgegapWindowMetadata.API_TOKEN_VERIFY_BTN_ID); _apiTokenGetBtn = rootVisualElement.Q<Button>(EdgegapWindowMetadata.API_TOKEN_GET_BTN_ID); _postAuthContainer = rootVisualElement.Q<VisualElement>(EdgegapWindowMetadata.POST_AUTH_CONTAINER_ID); _appInfoFoldout = rootVisualElement.Q<Foldout>(EdgegapWindowMetadata.APP_INFO_FOLDOUT_ID); _appNameInput = rootVisualElement.Q<TextField>(EdgegapWindowMetadata.APP_NAME_TXT_ID); _appLoadExistingBtn = rootVisualElement.Q<Button>(EdgegapWindowMetadata.APP_LOAD_EXISTING_BTN_ID); _appIconSpriteObjInput = rootVisualElement.Q<ObjectField>(EdgegapWindowMetadata.APP_ICON_SPRITE_OBJ_ID); _appCreateBtn = rootVisualElement.Q<Button>(EdgegapWindowMetadata.APP_CREATE_BTN_ID); _appCreateResultLabel = rootVisualElement.Q<Label>(EdgegapWindowMetadata.APP_CREATE_RESULT_LABEL_ID); _containerRegistryFoldout = rootVisualElement.Q<Foldout>(EdgegapWindowMetadata.CONTAINER_REGISTRY_FOLDOUT_ID); _containerNewTagVersionInput = rootVisualElement.Q<TextField>(EdgegapWindowMetadata.CONTAINER_NEW_TAG_VERSION_TXT_ID); _containerPortNumInput = rootVisualElement.Q<TextField>(EdgegapWindowMetadata.CONTAINER_REGISTRY_PORT_NUM_ID); // MIRROR CHANGE: dynamically resolving PortType fails if not in Assembly-CSharp-Editor.dll. Hardcode UDP/TCP/WS instead. // this finds the placeholder and dynamically replaces it with a popup field VisualElement dropdownPlaceholder = rootVisualElement.Q<VisualElement>("MIRROR_CHANGE_PORT_HARDCODED"); List<string> options = Enum.GetNames(typeof(ProtocolType)).Cast<string>().ToList(); _containerTransportTypeEnumInput = new PopupField<string>("Protocol Type", options, 0); dropdownPlaceholder.Add(_containerTransportTypeEnumInput); // END MIRROR CHANGE _containerUseCustomRegistryToggle = rootVisualElement.Q<Toggle>(EdgegapWindowMetadata.CONTAINER_USE_CUSTOM_REGISTRY_TOGGLE_ID); _containerCustomRegistryWrapper = rootVisualElement.Q<VisualElement>(EdgegapWindowMetadata.CONTAINER_CUSTOM_REGISTRY_WRAPPER_ID); _containerRegistryUrlInput = rootVisualElement.Q<TextField>(EdgegapWindowMetadata.CONTAINER_REGISTRY_URL_TXT_ID); _containerImageRepositoryInput = rootVisualElement.Q<TextField>(EdgegapWindowMetadata.CONTAINER_IMAGE_REPOSITORY_URL_TXT_ID); _containerUsernameInput = rootVisualElement.Q<TextField>(EdgegapWindowMetadata.CONTAINER_USERNAME_TXT_ID); _containerTokenInput = rootVisualElement.Q<TextField>(EdgegapWindowMetadata.CONTAINER_TOKEN_TXT_ID); _containerBuildAndPushServerBtn = rootVisualElement.Q<Button>(EdgegapWindowMetadata.CONTAINER_BUILD_AND_PUSH_BTN_ID); _containerBuildAndPushResultLabel = rootVisualElement.Q<Label>(EdgegapWindowMetadata.CONTAINER_BUILD_AND_PUSH_RESULT_LABEL_ID); _deploymentsFoldout = rootVisualElement.Q<Foldout>(EdgegapWindowMetadata.DEPLOYMENTS_FOLDOUT_ID); _deploymentsRefreshBtn = rootVisualElement.Q<Button>(EdgegapWindowMetadata.DEPLOYMENTS_REFRESH_BTN_ID); _deploymentsCreateBtn = rootVisualElement.Q<Button>(EdgegapWindowMetadata.DEPLOYMENTS_CREATE_BTN_ID); _deploymentsStatusLabel = rootVisualElement.Q<Label>(EdgegapWindowMetadata.DEPLOYMENTS_STATUS_LABEL_ID); _deploymentsServerDataContainer = rootVisualElement.Q<VisualElement>(EdgegapWindowMetadata.DEPLOYMENTS_CONTAINER_ID); // Dynamic _deploymentConnectionCopyUrlBtn = rootVisualElement.Q<Button>(EdgegapWindowMetadata.DEPLOYMENTS_CONNECTION_COPY_URL_BTN_ID); _deploymentsConnectionUrlReadonlyInput = rootVisualElement.Q<TextField>(EdgegapWindowMetadata.DEPLOYMENTS_CONNECTION_URL_READONLY_TXT_ID); _deploymentsConnectionStatusLabel = rootVisualElement.Q<Label>(EdgegapWindowMetadata.DEPLOYMENTS_CONNECTION_STATUS_LABEL_ID); _deploymentsConnectionStopBtn = rootVisualElement.Q<Button>(EdgegapWindowMetadata.DEPLOYMENTS_CONNECTION_SERVER_ACTION_STOP_BTN_ID); _footerDocumentationBtn = rootVisualElement.Q<Button>(EdgegapWindowMetadata.FOOTER_DOCUMENTATION_BTN_ID); _footerNeedMoreGameServersBtn = rootVisualElement.Q<Button>(EdgegapWindowMetadata.FOOTER_NEED_MORE_GAME_SERVERS_BTN_ID); _apiEnvironment = EdgegapWindowMetadata.API_ENVIRONMENT; // (!) TODO: Hard-coded while unused in UI } /// <summary> /// Sanity check: If we implicitly changed an #Id, we need to know early so we can update the const. /// In order of appearance seen in setVisualElementsToFields(). /// </summary> private void assertVisualElementKeys() { // MIRROR CHANGE: this doesn't compile in Unity 2019 /* try { Assert.IsTrue(_apiTokenInput is { name: EdgegapWindowMetadata.API_TOKEN_TXT_ID }, $"Expected {nameof(_apiTokenInput)} via #{EdgegapWindowMetadata.API_TOKEN_TXT_ID}"); Assert.IsTrue(_apiTokenVerifyBtn is { name: EdgegapWindowMetadata.API_TOKEN_VERIFY_BTN_ID }, $"Expected {nameof(_apiTokenVerifyBtn)} via #{EdgegapWindowMetadata.API_TOKEN_VERIFY_BTN_ID}"); Assert.IsTrue(_apiTokenGetBtn is { name: EdgegapWindowMetadata.API_TOKEN_GET_BTN_ID }, $"Expected {nameof(_apiTokenGetBtn)} via #{EdgegapWindowMetadata.API_TOKEN_GET_BTN_ID}"); Assert.IsTrue(_postAuthContainer is { name: EdgegapWindowMetadata.POST_AUTH_CONTAINER_ID }, $"Expected {nameof(_postAuthContainer)} via #{EdgegapWindowMetadata.POST_AUTH_CONTAINER_ID}"); Assert.IsTrue(_appInfoFoldout is { name: EdgegapWindowMetadata.APP_INFO_FOLDOUT_ID }, $"Expected {nameof(_appInfoFoldout)} via #{EdgegapWindowMetadata.APP_INFO_FOLDOUT_ID}"); Assert.IsTrue(_appNameInput is { name: EdgegapWindowMetadata.APP_NAME_TXT_ID }, $"Expected {nameof(_appNameInput)} via #{EdgegapWindowMetadata.APP_NAME_TXT_ID}"); Assert.IsTrue(_appLoadExistingBtn is { name: EdgegapWindowMetadata.APP_LOAD_EXISTING_BTN_ID }, $"Expected {nameof(_appLoadExistingBtn)} via #{EdgegapWindowMetadata.APP_LOAD_EXISTING_BTN_ID}"); Assert.IsTrue(_appIconSpriteObjInput is { name: EdgegapWindowMetadata.APP_ICON_SPRITE_OBJ_ID }, $"Expected {nameof(_appIconSpriteObjInput)} via #{EdgegapWindowMetadata.APP_ICON_SPRITE_OBJ_ID}"); Assert.IsTrue(_appCreateBtn is { name: EdgegapWindowMetadata.APP_CREATE_BTN_ID }, $"Expected {nameof(_appCreateBtn)} via #{EdgegapWindowMetadata.APP_CREATE_BTN_ID}"); Assert.IsTrue(_appCreateResultLabel is { name: EdgegapWindowMetadata.APP_CREATE_RESULT_LABEL_ID }, $"Expected {nameof(_appCreateResultLabel)} via #{EdgegapWindowMetadata.APP_CREATE_RESULT_LABEL_ID}"); Assert.IsTrue(_containerRegistryFoldout is { name: EdgegapWindowMetadata.CONTAINER_REGISTRY_FOLDOUT_ID }, $"Expected {nameof(_containerRegistryFoldout)} via #{EdgegapWindowMetadata.CONTAINER_REGISTRY_FOLDOUT_ID}"); Assert.IsTrue(_containerPortNumInput is { name: EdgegapWindowMetadata.CONTAINER_REGISTRY_PORT_NUM_ID }, $"Expected {nameof(_containerPortNumInput)} via #{EdgegapWindowMetadata.CONTAINER_REGISTRY_PORT_NUM_ID}"); // MIRROR CHANGE: disable and replaced with hardcoded port type dropdown // Assert.IsTrue(_containerTransportTypeEnumInput is { name: EdgegapWindowMetadata.CONTAINER_REGISTRY_TRANSPORT_TYPE_ENUM_ID }, // $"Expected {nameof(_containerTransportTypeEnumInput)} via #{EdgegapWindowMetadata.CONTAINER_REGISTRY_TRANSPORT_TYPE_ENUM_ID}"); // END MIRROR CHANGE Assert.IsTrue(_containerUseCustomRegistryToggle is { name: EdgegapWindowMetadata.CONTAINER_USE_CUSTOM_REGISTRY_TOGGLE_ID }, $"Expected {nameof(_containerUseCustomRegistryToggle)} via #{EdgegapWindowMetadata.CONTAINER_USE_CUSTOM_REGISTRY_TOGGLE_ID}"); Assert.IsTrue(_containerCustomRegistryWrapper is { name: EdgegapWindowMetadata.CONTAINER_CUSTOM_REGISTRY_WRAPPER_ID }, $"Expected {nameof(_containerCustomRegistryWrapper)} via #{EdgegapWindowMetadata.CONTAINER_CUSTOM_REGISTRY_WRAPPER_ID}"); Assert.IsTrue(_containerRegistryUrlInput is { name: EdgegapWindowMetadata.CONTAINER_REGISTRY_URL_TXT_ID }, $"Expected {nameof(_containerRegistryUrlInput)} via #{EdgegapWindowMetadata.CONTAINER_REGISTRY_URL_TXT_ID}"); Assert.IsTrue(_containerImageRepositoryInput is { name: EdgegapWindowMetadata.CONTAINER_IMAGE_REPOSITORY_URL_TXT_ID }, $"Expected {nameof(_containerImageRepositoryInput)} via #{EdgegapWindowMetadata.CONTAINER_IMAGE_REPOSITORY_URL_TXT_ID}"); Assert.IsTrue(_containerUsernameInput is { name: EdgegapWindowMetadata.CONTAINER_USERNAME_TXT_ID }, $"Expected {nameof(_containerUsernameInput)} via #{EdgegapWindowMetadata.CONTAINER_USERNAME_TXT_ID}"); Assert.IsTrue(_containerTokenInput is { name: EdgegapWindowMetadata.CONTAINER_TOKEN_TXT_ID }, $"Expected {nameof(_containerTokenInput)} via #{EdgegapWindowMetadata.CONTAINER_TOKEN_TXT_ID}"); Assert.IsTrue(_containerTokenInput is { name: EdgegapWindowMetadata.CONTAINER_TOKEN_TXT_ID }, $"Expected {nameof(_containerTokenInput)} via #{EdgegapWindowMetadata.CONTAINER_TOKEN_TXT_ID}"); Assert.IsTrue(_containerBuildAndPushResultLabel is { name: EdgegapWindowMetadata.CONTAINER_BUILD_AND_PUSH_RESULT_LABEL_ID }, $"Expected {nameof(_containerBuildAndPushResultLabel)} via #{EdgegapWindowMetadata.CONTAINER_BUILD_AND_PUSH_RESULT_LABEL_ID}"); Assert.IsTrue(_deploymentsFoldout is { name: EdgegapWindowMetadata.DEPLOYMENTS_FOLDOUT_ID }, $"Expected {nameof(_deploymentsFoldout)} via #{EdgegapWindowMetadata.DEPLOYMENTS_FOLDOUT_ID}"); Assert.IsTrue(_deploymentsRefreshBtn is { name: EdgegapWindowMetadata.DEPLOYMENTS_REFRESH_BTN_ID }, $"Expected {nameof(_deploymentsRefreshBtn)} via #{EdgegapWindowMetadata.DEPLOYMENTS_REFRESH_BTN_ID}"); Assert.IsTrue(_deploymentsCreateBtn is { name: EdgegapWindowMetadata.DEPLOYMENTS_CREATE_BTN_ID }, $"Expected {nameof(_deploymentsCreateBtn)} via #{EdgegapWindowMetadata.DEPLOYMENTS_CREATE_BTN_ID}"); Assert.IsTrue(_deploymentsStatusLabel is { name: EdgegapWindowMetadata.DEPLOYMENTS_STATUS_LABEL_ID }, $"Expected {nameof(_deploymentsStatusLabel)} via #{EdgegapWindowMetadata.DEPLOYMENTS_STATUS_LABEL_ID}"); Assert.IsTrue(_deploymentsServerDataContainer is { name: EdgegapWindowMetadata.DEPLOYMENTS_CONTAINER_ID }, $"Expected {nameof(_deploymentsServerDataContainer)} via #{EdgegapWindowMetadata.DEPLOYMENTS_CONTAINER_ID}"); Assert.IsTrue(_deploymentConnectionCopyUrlBtn is { name: EdgegapWindowMetadata.DEPLOYMENTS_CONNECTION_COPY_URL_BTN_ID }, $"Expected {nameof(_deploymentConnectionCopyUrlBtn)} via #{EdgegapWindowMetadata.DEPLOYMENTS_CONNECTION_COPY_URL_BTN_ID}"); Assert.IsTrue(_deploymentsConnectionUrlReadonlyInput is { name: EdgegapWindowMetadata.DEPLOYMENTS_CONNECTION_URL_READONLY_TXT_ID }, $"Expected {nameof(_deploymentsConnectionUrlReadonlyInput)} via #{EdgegapWindowMetadata.DEPLOYMENTS_CONNECTION_URL_READONLY_TXT_ID}"); Assert.IsTrue(_deploymentsConnectionStatusLabel is { name: EdgegapWindowMetadata.DEPLOYMENTS_CONNECTION_STATUS_LABEL_ID }, $"Expected {nameof(_deploymentsConnectionStatusLabel)} via #{EdgegapWindowMetadata.DEPLOYMENTS_CONNECTION_STATUS_LABEL_ID}"); Assert.IsTrue(_deploymentsConnectionStopBtn is { name: EdgegapWindowMetadata.DEPLOYMENTS_CONNECTION_SERVER_ACTION_STOP_BTN_ID }, $"Expected {nameof(_deploymentsConnectionStopBtn)} via #{EdgegapWindowMetadata.DEPLOYMENTS_CONNECTION_SERVER_ACTION_STOP_BTN_ID}"); Assert.IsTrue(_footerDocumentationBtn is { name: EdgegapWindowMetadata.FOOTER_DOCUMENTATION_BTN_ID }, $"Expected {nameof(_footerDocumentationBtn)} via #{EdgegapWindowMetadata.FOOTER_DOCUMENTATION_BTN_ID}"); Assert.IsTrue(_footerNeedMoreGameServersBtn is { name: EdgegapWindowMetadata.FOOTER_NEED_MORE_GAME_SERVERS_BTN_ID }, $"Expected {nameof(_footerNeedMoreGameServersBtn)} via #{EdgegapWindowMetadata.FOOTER_NEED_MORE_GAME_SERVERS_BTN_ID}"); // // TODO: Explicitly set, for now in v2 - but remember to assert later if we stop hard-coding these >> // _apiEnvironment // _appVersionName } catch (Exception e) { Debug.LogError(e.Message); _postAuthContainer.SetEnabled(false); } */ // END MIRROR CHANGE } /// <summary> /// Register non-btn change actionss. We'll want to save for persistence, validate, etc /// </summary> private void registerFieldCallbacks() { _apiTokenInput.RegisterValueChangedCallback(onApiTokenInputChanged); _apiTokenInput.RegisterCallback<FocusOutEvent>(onApiTokenInputFocusOut); _appNameInput.RegisterValueChangedCallback(onAppNameInputChanged); _containerPortNumInput.RegisterCallback<FocusOutEvent>(onContainerPortNumInputFocusOut); _containerUseCustomRegistryToggle.RegisterValueChangedCallback(onContainerUseCustomRegistryToggle); _containerNewTagVersionInput.RegisterValueChangedCallback(onContainerNewTagVersionInputChanged); } /// <summary> /// Prevents memory leaks, mysterious errors and "ghost" values set from a previous session. /// Should parity the opposute of registerFieldCallbacks(). /// </summary> private void unregisterFieldCallbacks() { _apiTokenInput.UnregisterValueChangedCallback(onApiTokenInputChanged); _apiTokenInput.UnregisterCallback<FocusOutEvent>(onApiTokenInputFocusOut); _containerUseCustomRegistryToggle.UnregisterValueChangedCallback(onContainerUseCustomRegistryToggle); _containerPortNumInput.UnregisterCallback<FocusOutEvent>(onContainerPortNumInputFocusOut); } /// <summary> /// Register click actions, mostly from buttons: Need to -= unregistry them @ OnDisable() /// </summary> private void registerClickCallbacks() { _debugBtn.clickable.clicked += onDebugBtnClick; _apiTokenVerifyBtn.clickable.clicked += onApiTokenVerifyBtnClick; _apiTokenGetBtn.clickable.clicked += onApiTokenGetBtnClick; _appCreateBtn.clickable.clicked += onAppCreateBtnClickAsync; _appLoadExistingBtn.clickable.clicked += onAppLoadExistingBtnClickAsync; _containerBuildAndPushServerBtn.clickable.clicked += onContainerBuildAndPushServerBtnClickAsync; _deploymentConnectionCopyUrlBtn.clickable.clicked += onDeploymentConnectionCopyUrlBtnClick; _deploymentsRefreshBtn.clickable.clicked += onDeploymentsRefreshBtnClick; _deploymentsCreateBtn.clickable.clicked += onDeploymentCreateBtnClick; _footerDocumentationBtn.clickable.clicked += onFooterDocumentationBtnClick; _footerNeedMoreGameServersBtn.clickable.clicked += onFooterNeedMoreGameServersBtnClick; } /// <summary> /// Prevents memory leaks, mysterious errors and "ghost" values set from a previous session. /// Should parity the opposute of registerClickEvents(). /// </summary> private void unregisterClickEvents() { _debugBtn.clickable.clicked -= onDebugBtnClick; _apiTokenVerifyBtn.clickable.clicked -= onApiTokenVerifyBtnClick; _apiTokenGetBtn.clickable.clicked -= onApiTokenGetBtnClick; _appCreateBtn.clickable.clicked -= onAppCreateBtnClickAsync; _appLoadExistingBtn.clickable.clicked -= onAppLoadExistingBtnClickAsync; _containerBuildAndPushServerBtn.clickable.clicked -= onContainerBuildAndPushServerBtnClickAsync; _deploymentConnectionCopyUrlBtn.clickable.clicked -= onDeploymentConnectionCopyUrlBtnClick; _deploymentsRefreshBtn.clickable.clicked -= onDeploymentsRefreshBtnClick; _deploymentsCreateBtn.clickable.clicked -= onDeploymentCreateBtnClick; _footerDocumentationBtn.clickable.clicked -= onFooterDocumentationBtnClick; _footerNeedMoreGameServersBtn.clickable.clicked -= onFooterNeedMoreGameServersBtnClick; } private void initToggleDynamicUi() { hideResultLabels(); _deploymentsRefreshBtn.SetEnabled(false); loadPersistentDataFromEditorPrefs(); setDeploymentBtnsFromCache(); _debugBtn.visible = EdgegapWindowMetadata.SHOW_DEBUG_BTN; } /// <summary> /// Based on existing _deploymentsConnection[Url|Status]Label txt /// </summary> private void setDeploymentBtnsFromCache() { bool showDeploymentConnectionStopBtn = !string.IsNullOrEmpty(_deploymentsConnectionUrlReadonlyInput.text); if (!showDeploymentConnectionStopBtn) return; // We found some leftover connection cache >> _deploymentsConnectionStopBtn.visible = true; _deploymentsRefreshBtn.SetEnabled(true); // Enable stop btn? bool isDeployed = _deploymentsConnectionStatusLabel.text.ToLowerInvariant().Contains("deployed"); _deploymentsConnectionStopBtn.SetEnabled(isDeployed); if (isDeployed) _deploymentsConnectionStopBtn.clickable.clickedWithEventInfo += onDynamicStopServerBtnAsync; // Unsub'd from within } /// <summary>For example, result labels (success/err) should be hidden on init</summary> private void hideResultLabels() { _appCreateResultLabel.visible = false; _containerBuildAndPushResultLabel.visible = false; _deploymentsStatusLabel.style.display = DisplayStyle.None; } #region Init -> Button clicks /// <summary> /// Experiment here! You may want to log what you're doing /// in case you inadvertently leave it on. /// </summary> private void onDebugBtnClick() => debugEnableAllGroups(); private void debugEnableAllGroups() { Debug.Log("debugEnableAllGroups"); _appInfoFoldout.SetEnabled(true); _appInfoFoldout.SetEnabled(true); _containerRegistryFoldout.SetEnabled(true); _deploymentsFoldout.SetEnabled(true); if (_containerUseCustomRegistryToggle.value) _containerCustomRegistryWrapper.SetEnabled(true); } private void onApiTokenVerifyBtnClick() => _ = verifyApiTokenGetRegistryCredsAsync(); private void onApiTokenGetBtnClick() => openGetApiTokenWebsite(); /// <summary>Process UI + validation before/after API logic</summary> private async void onAppCreateBtnClickAsync() { // Assert data locally before calling API assertAppNameExists(); _appCreateResultLabel.text = EdgegapWindowMetadata.WrapRichTextInColor("Creating...", EdgegapWindowMetadata.StatusColors.Processing); try { await createAppAsync(); } finally { _appCreateBtn.SetEnabled(checkHasAppName()); _appCreateResultLabel.visible = _appCreateResultLabel.text != EdgegapWindowMetadata.LOADING_RICH_STR; } } /// <summary>Process UI + validation before/after API logic</summary> private async void onAppLoadExistingBtnClickAsync() { // Assert UI data locally before calling API assertAppNameExists(); try { await GetAppAsync(); } finally { _appLoadExistingBtn.SetEnabled(checkHasAppName()); _appCreateResultLabel.visible = _appCreateResultLabel.text != EdgegapWindowMetadata.LOADING_RICH_STR; } } /// <summary>Copy url to clipboard</summary> private void onDeploymentConnectionCopyUrlBtnClick() { if (string.IsNullOrEmpty(_deploymentsConnectionUrlReadonlyInput.value)) return; // Nothing to copy EditorGUIUtility.systemCopyBuffer = _deploymentsConnectionUrlReadonlyInput.value; _deploymentsStatusLabel.text = EdgegapWindowMetadata.WrapRichTextInColor("Copied URL!", EdgegapWindowMetadata.StatusColors.Success); _deploymentsStatusLabel.style.display = DisplayStyle.Flex; _ = clearDeploymentStatusAfterDelay(seconds: 1); } private async Task clearDeploymentStatusAfterDelay(int seconds) { await Task.Delay(TimeSpan.FromSeconds(seconds)); _deploymentsStatusLabel.style.display = DisplayStyle.None; } /// <summary>Process UI + validation before/after API logic</summary> private async void onContainerBuildAndPushServerBtnClickAsync() { // Assert data locally before calling API // Validate custom container registry, app name try { assertAppNameExists(); Assert.IsTrue( !_containerImageRepositoryInput.value.EndsWith("/"), $"Expected {nameof(_containerImageRepositoryInput)} to !contain " + "trailing slash (should end with /appName)"); } catch (Exception e) { Debug.LogError($"onContainerBuildAndPushServerBtnClickAsync Error: {e}"); throw; } // Hide previous result labels, disable btns (to reenable when done) hideResultLabels(); _containerBuildAndPushServerBtn.SetEnabled(false); // Show new loading status _containerBuildAndPushResultLabel.text = EdgegapWindowMetadata.WrapRichTextInColor( EdgegapWindowMetadata.PROCESSING_RICH_STR, EdgegapWindowMetadata.StatusColors.Processing); try { await buildAndPushServerAsync(); } finally { _containerBuildAndPushServerBtn.SetEnabled(checkHasAppName()); _containerBuildAndPushResultLabel.visible = _containerBuildAndPushResultLabel.text != EdgegapWindowMetadata.PROCESSING_RICH_STR; } } private bool checkHasAppName() => _appNameInput.value.Length > 0; private void onDeploymentsRefreshBtnClick() => _ = refreshDeploymentsAsync(); private void onFooterDocumentationBtnClick() => openDocumentationWebsite(); private void onFooterNeedMoreGameServersBtnClick() => openNeedMoreGameServersWebsite(); /// <summary>AKA "Create New Deployment" Btn</summary> private void onDeploymentCreateBtnClick() => _ = createDeploymentStartServerAsync(); #endregion // Init -> /Button Clicks #endregion // Init /// <summary>Throw if !appName val</summary> private void assertAppNameExists() => Assert.IsTrue(!string.IsNullOrEmpty(_appNameInput.value), $"Expected {nameof(_appNameInput)} val"); /// <summary> /// Save persistent read-only data: If the human didn't type it, it won't save automatically. /// </summary> private void SyncObjectWithForm() { _appIconSpriteObj = _appIconSpriteObjInput.value as Sprite; } /// <summary>TODO: Load persistent data?</summary> private void syncFormWithObjectStatic() { // Only show the rest of the form if apiToken is verified _postAuthContainer.SetEnabled(_isApiTokenVerified); _appIconSpriteObjInput.value = _appIconSpriteObj; _containerCustomRegistryWrapper.SetEnabled(_containerUseCustomRegistryToggle.value); _containerUseCustomRegistryToggle.value = _containerUseCustomRegistryToggle.value; // Only enable certain elements if appName exists bool hasAppName = checkHasAppName(); _appCreateBtn.SetEnabled(hasAppName); _appLoadExistingBtn.SetEnabled(hasAppName); } /// <summary> /// Dynamically set form based on API call results. /// => If APIToken is cached via EditorPrefs, verify => gets registry creds. /// => If appName is cached via ViewDataKey, loads the app. /// </summary> private async Task syncFormWithObjectDynamicAsync() { if (string.IsNullOrEmpty(_apiTokenInput.value)) return; // We found a cached api token: Verify => if (IsLogLevelDebug) Debug.Log("syncFormWithObjectDynamicAsync: Found apiToken; " + "calling verifyApiTokenGetRegistryCredsAsync =>"); await verifyApiTokenGetRegistryCredsAsync(); // Was the API token verified + we found a cached app name? Load the app => // But ignore errs, since we're just *assuming* the app exists since the appName was filled if (_isApiTokenVerified && checkHasAppName()) { if (IsLogLevelDebug) Debug.Log("syncFormWithObjectDynamicAsync: Found apiToken && appName; " + "calling GetAppAsync =>"); try { await GetAppAsync(); } finally { _appLoadExistingBtn.SetEnabled(checkHasAppName()); } } } #region Immediate non-button changes /// <summary> /// On change, validate -> update custom container registry suffix. /// Toggle create app btn if 1+ char /// </summary> /// <param name="evt"></param> private void onAppNameInputChanged(ChangeEvent<string> evt) { // Validate: Only allow alphanumeric, underscore, dash, plus, period if (!_appNameAllowedCharsRegex.IsMatch(evt.newValue)) _appNameInput.value = evt.previousValue; // Revert to the previous value else setContainerImageRepositoryVal(); // Valid -> Update the custom container registry suffix // Toggle btns on 1+ char entered bool hasAppName = checkHasAppName(); _appCreateBtn.SetEnabled(hasAppName); _appLoadExistingBtn.SetEnabled(hasAppName); } /// <summary>On focus out, clamp port between 1024~49151</summary> /// <param name="evt"></param> private void onContainerPortNumInputFocusOut(FocusOutEvent evt) { // Use TryParse to avoid exceptions if (int.TryParse(_containerPortNumInput.value, out int port)) { // Clamp the port to the range and set the value back to the TextField _containerPortNumInput.value = Mathf.Clamp( port, EdgegapWindowMetadata.PORT_MIN, EdgegapWindowMetadata.PORT_MAX) .ToString(); } else { // If input is !valid, set to default _containerPortNumInput.value = EdgegapWindowMetadata.PORT_DEFAULT.ToString(); } } /// <summary> /// While changing the token, we temporarily unmask. On change, set state to !verified. /// </summary> /// <param name="evt"></param> private void onApiTokenInputChanged(ChangeEvent<string> evt) { // Unmask while changing TextField apiTokenTxt = evt.target as TextField; apiTokenTxt.isPasswordField = false; // Token changed? Reset form to !verified state and fold all groups _isApiTokenVerified = false; _postAuthContainer.SetEnabled(false); closeDisableGroups(); // Toggle "Verify" btn on 1+ char entered _apiTokenVerifyBtn.SetEnabled(evt.newValue.Length > 0); } /// <summary>Unmask while typing</summary> /// <param name="evt"></param> private void onApiTokenInputFocusOut(FocusOutEvent evt) { TextField apiTokenTxt = evt.target as TextField; apiTokenTxt.isPasswordField = true; } /// <summary>On toggle, enable || disable the custom registry inputs (below the Toggle).</summary> private void onContainerUseCustomRegistryToggle(ChangeEvent<bool> evt) => _containerCustomRegistryWrapper.SetEnabled(evt.newValue); /// <summary>On empty, we fallback to "latest", a fallback val from EdgegapWindowMetadata.cs</summary> /// <param name="evt"></param> private void onContainerNewTagVersionInputChanged(ChangeEvent<string> evt) { if (!string.IsNullOrEmpty(evt.newValue)) return; // Set fallback value -> select all for UX, since the user may not expect this _containerNewTagVersionInput.value = EdgegapWindowMetadata.DEFAULT_VERSION_TAG; _containerNewTagVersionInput.SelectAll(); } #endregion // Immediate non-button changes /// <summary> /// Used for converting a Sprite to a base64 string: By default, textures are !readable, /// and we don't want to have to instruct users how to make it readable for UX. /// Instead, we'll make a copy of that texture -> make it readable. /// </summary> /// <param name="original"></param> /// <returns></returns> private Texture2D makeTextureReadable(Texture2D original) { RenderTexture rt = RenderTexture.GetTemporary( original.width, original.height ); Graphics.Blit(original, rt); Texture2D readableTexture = new Texture2D(original.width, original.height); Rect rect = new Rect( 0, 0, rt.width, rt.height); readableTexture.ReadPixels(rect, destX: 0, destY: 0); readableTexture.Apply(); RenderTexture.ReleaseTemporary(rt); return readableTexture; } /// <summary>From Base64 string -> to Sprite</summary> /// <param name="imgBase64Str">Edgegap build app requires a max size of 200</param> /// <returns>Sprite</returns> private Sprite getSpriteFromBase64Str(string imgBase64Str) { if (string.IsNullOrEmpty(imgBase64Str)) return null; try { byte[] imageBytes = Convert.FromBase64String(imgBase64Str); Texture2D texture = new Texture2D(2, 2); texture.LoadImage(imageBytes); Rect rect = new Rect( // MIRROR CHANGE: 'new()' not supported in Unity 2020 x: 0.0f, y: 0.0f, texture.width, texture.height); return Sprite.Create( texture, rect, pivot: new Vector2(0.5f, 0.5f), pixelsPerUnit: 100.0f); } catch (Exception e) { Debug.Log($"Warning: getSpriteFromBase64Str failed (returning null) - {e}"); return null; } } /// <summary>From Sprite -> to Base64 string</summary> /// <param name="sprite"></param> /// <param name="maxKbSize">Edgegap build app requires a max size of 200</param> /// <returns>imageBase64Str</returns> private string getBase64StrFromSprite(Sprite sprite, int maxKbSize = 200) { if (sprite == null) return null; try { Texture2D texture = makeTextureReadable(sprite.texture); // Crop the texture to the sprite's rectangle (instead of the entire texture) Texture2D croppedTexture = new Texture2D( (int)sprite.rect.width, (int)sprite.rect.height); Color[] pixels = texture.GetPixels( (int)sprite.rect.x, (int)sprite.rect.y, (int)sprite.rect.width, (int)sprite.rect.height ); croppedTexture.SetPixels(pixels); croppedTexture.Apply(); // Encode to PNG -> byte[] textureBytes = croppedTexture.EncodeToPNG(); // Validate size const int oneKb = 1024; int pngTextureSizeKb = textureBytes.Length / oneKb; bool isPngLessThanMaxSize = pngTextureSizeKb < maxKbSize; if (!isPngLessThanMaxSize) { textureBytes = croppedTexture.EncodeToJPG(); int jpgTextureSizeKb = textureBytes.Length / oneKb; bool isJpgLessThanMaxSize = pngTextureSizeKb < maxKbSize; Assert.IsTrue(isJpgLessThanMaxSize, $"Expected texture PNG to be < {maxKbSize}kb " + $"in size (but found {jpgTextureSizeKb}kb); then tried JPG, but is still {jpgTextureSizeKb}kb in size"); Debug.LogWarning($"App icon PNG was too large (max {maxKbSize}), so we converted to JPG"); } string base64ImageString = Convert.ToBase64String(textureBytes); // eg: "Aaabbcc==" return base64ImageString; } catch (Exception e) { Debug.LogError($"Error: {e.Message}"); return null; } } /// <summary> /// Verifies token => apps/container groups -> gets registry creds (if any). /// TODO: UX - Show loading spinner. /// </summary> private async Task verifyApiTokenGetRegistryCredsAsync() { if (IsLogLevelDebug) Debug.Log("verifyApiTokenGetRegistryCredsAsync"); // Disable most ui while we verify _isApiTokenVerified = false; _apiTokenVerifyBtn.SetEnabled(false); SyncContainerEnablesToState(); hideResultLabels(); EdgegapWizardApi wizardApi = getWizardApi(); EdgegapHttpResult initQuickStartResultCode = await wizardApi.InitQuickStart(); _apiTokenVerifyBtn.SetEnabled(true); _isApiTokenVerified = initQuickStartResultCode.IsResultCode204; if (!_isApiTokenVerified) { SyncContainerEnablesToState(); return; } // Verified: Let's see if we have active registry credentials // TODO: This will later be a result model EdgegapHttpResult<GetRegistryCredentialsResult> getRegistryCredentialsResult = await wizardApi.GetRegistryCredentials(); if (getRegistryCredentialsResult.IsResultCode200) { // Success _credentials = getRegistryCredentialsResult.Data; persistUnmaskedApiToken(_apiTokenInput.value); prefillContainerRegistryForm(_credentials); } else { // Fail } // Unlock the rest of the form, whether we prefill the container registry or not SyncContainerEnablesToState(); } /// <summary> /// We have container registry params; we'll prefill registry container fields. /// </summary> /// <param name="credentials">GetRegistryCredentialsResult</param> private void prefillContainerRegistryForm(GetRegistryCredentialsResult credentials) { if (IsLogLevelDebug) Debug.Log("prefillContainerRegistryForm"); if (credentials == null) throw new Exception($"!{nameof(credentials)}"); _containerRegistryUrlInput.value = credentials.RegistryUrl; setContainerImageRepositoryVal(); _containerUsernameInput.value = credentials.Username; _containerTokenInput.value = credentials.Token; } /// <summary> /// Sets to "{credentials.Project}/{appName}" from cached credentials, forcing lowercased appName. /// </summary> private void setContainerImageRepositoryVal() { // ex: "xblade1-9sa8dfh9sda8hf/mygame1" string project = _credentials?.Project ?? ""; string appName = _appNameInput?.value.ToLowerInvariant() ?? ""; _containerImageRepositoryInput.value = $"{project}/{appName}"; } public static string Base64Encode(string plainText) { byte[] plainBytes = Encoding.UTF8.GetBytes(plainText); return Convert.ToBase64String(plainBytes); } public static string Base64Decode(string base64EncodedText) { byte[] base64Bytes = Convert.FromBase64String(base64EncodedText); return Encoding.UTF8.GetString(base64Bytes); } /// <summary> /// Toggle container groups and foldouts on/off based on: /// - _isApiTokenVerified /// </summary> private void SyncContainerEnablesToState() { // Requires _isApiTokenVerified _postAuthContainer.SetEnabled(_isApiTokenVerified); // Entire body container _appInfoFoldout.SetEnabled(_isApiTokenVerified); _appInfoFoldout.value = _isApiTokenVerified; // + Requires _isContainerRegistryReady bool isApiTokenVerifiedAndContainerReady = _isApiTokenVerified && _isContainerRegistryReady; _containerRegistryFoldout.SetEnabled(isApiTokenVerifiedAndContainerReady); _containerRegistryFoldout.value = isApiTokenVerifiedAndContainerReady; _deploymentsFoldout.SetEnabled(isApiTokenVerifiedAndContainerReady); _deploymentsFoldout.value = isApiTokenVerifiedAndContainerReady; // + Requires _containerUseCustomRegistryToggleBool _containerCustomRegistryWrapper.SetEnabled(isApiTokenVerifiedAndContainerReady && _containerUseCustomRegistryToggle.value); } private void openGetApiTokenWebsite() { if (IsLogLevelDebug) Debug.Log("openGetApiTokenWebsite"); Application.OpenURL(EdgegapWindowMetadata.EDGEGAP_GET_A_TOKEN_URL); } /// <returns>isSuccess; sets _isContainerRegistryReady + _loadedApp</returns> private async Task<bool> GetAppAsync() { if (IsLogLevelDebug) Debug.Log("GetAppAsync"); // Hide previous result labels, disable btns (to reenable when done) hideResultLabels(); _appCreateBtn.SetEnabled(false); _apiTokenVerifyBtn.SetEnabled(false); // Show new loading status _appCreateResultLabel.text = EdgegapWindowMetadata.WrapRichTextInColor( EdgegapWindowMetadata.LOADING_RICH_STR, EdgegapWindowMetadata.StatusColors.Processing); _appCreateResultLabel.visible = true; EdgegapAppApi appApi = getAppApi(); EdgegapHttpResult<GetCreateAppResult> getAppResult = await appApi.GetApp(_appNameInput.value); onGetCreateApplicationResult(getAppResult); return _isContainerRegistryReady; } /// <summary> /// TODO: Add err handling for reaching app limit (max 2 for free tier). /// </summary> private async Task createAppAsync() { if (IsLogLevelDebug) Debug.Log("createAppAsync"); // Hide previous result labels, disable btns (to reenable when done) hideResultLabels(); _appCreateBtn.SetEnabled(false); _apiTokenVerifyBtn.SetEnabled(false); EdgegapAppApi appApi = getAppApi(); CreateAppRequest createAppRequest = new CreateAppRequest( // MIRROR CHANGE: 'new()' not supported in Unity 2020 _appNameInput.value, isActive: true, getBase64StrFromSprite(_appIconSpriteObj) ?? ""); EdgegapHttpResult<GetCreateAppResult> createAppResult = await appApi.CreateApp(createAppRequest); onGetCreateApplicationResult(createAppResult); } /// <summary>Get || Create results both handled here. On success, sets _isContainerRegistryReady + _loadedApp data</summary> /// <param name="result"></param> private void onGetCreateApplicationResult(EdgegapHttpResult<GetCreateAppResult> result) { // Assert the result itself || result's create time exists bool isSuccess = result.IsResultCode200 || result.IsResultCode409; // 409 == app already exists _isContainerRegistryReady = isSuccess; _loadedApp = result.Data; _appCreateResultLabel.text = getFriendlyCreateAppResultStr(result); _containerRegistryFoldout.value = _isContainerRegistryReady; _appCreateBtn.SetEnabled(true); _apiTokenVerifyBtn.SetEnabled(true); SyncContainerEnablesToState(); // Only show status label if we're init'd; otherwise, we auto-tried to get the existing app that // we knew had a chance of not being there _appCreateResultLabel.visible = IsInitd; // App base64 img? Parse to sprite, overwrite app image UI/cache if (!string.IsNullOrEmpty(_loadedApp.Image)) { _appIconSpriteObj = getSpriteFromBase64Str(_loadedApp.Image); _appIconSpriteObjInput.value = _appIconSpriteObj; } // On fail, shake the "Add more game servers" btn // 400 == # of apps limit reached bool isCreate = result.HttpMethod == HttpMethod.Post; bool isCreateFailAppNumCapMaxed = isCreate && !_isContainerRegistryReady && result.IsResultCode400; if (isCreateFailAppNumCapMaxed) shakeNeedMoreGameServersBtn(); } /// <summary>Slight animation shake</summary> private void shakeNeedMoreGameServersBtn() { ButtonShaker shaker = new ButtonShaker(_footerNeedMoreGameServersBtn); _ = shaker.ApplyShakeAsync(); } /// <returns>Generally "Success" || "Error: {error}" || "Warning: {error}"</returns> private string getFriendlyCreateAppResultStr(EdgegapHttpResult<GetCreateAppResult> createAppResult) { string coloredResultStr = null; if (!_isContainerRegistryReady) { // Error string resultStr = $"<b>Error:</b> {createAppResult?.Error?.ErrorMessage}"; coloredResultStr = EdgegapWindowMetadata.WrapRichTextInColor( resultStr, EdgegapWindowMetadata.StatusColors.Error); } else if (createAppResult.IsResultCode409) { // Warn: App already exists - Still success, but just a warn string resultStr = $"<b>Warning:</b> {createAppResult.Error.ErrorMessage}"; coloredResultStr = EdgegapWindowMetadata.WrapRichTextInColor( resultStr, EdgegapWindowMetadata.StatusColors.Warn); } else { // Success coloredResultStr = EdgegapWindowMetadata.WrapRichTextInColor( "Success", EdgegapWindowMetadata.StatusColors.Success); } return coloredResultStr; } /// <summary>Open contact form in desired locale</summary> private void openNeedMoreGameServersWebsite() => Application.OpenURL(EdgegapWindowMetadata.EDGEGAP_ADD_MORE_GAME_SERVERS_URL); private void openDocumentationWebsite() { // MIRROR CHANGE /* string documentationUrl = _apiEnvironment.GetDocumentationUrl(); if (!string.IsNullOrEmpty(documentationUrl)) Application.OpenURL(documentationUrl); else { string apiEnvName = Enum.GetName(typeof(ApiEnvironment), _apiEnvironment); Debug.LogWarning($"Could not open documentation for api environment " + $"{apiEnvName}: No documentation URL."); } */ // link to our step by step guide Application.OpenURL("https://mirror-networking.gitbook.io/docs/hosting/edgegap-hosting-plugin-guide"); // END MIRROR CHANGE } /// <summary> /// Currently only refreshes an existing deployment. AKA "OnRefresh". /// TODO: Consider dynamically adding the entire list via GET all deployments. /// </summary> private async Task refreshDeploymentsAsync() { if (IsLogLevelDebug) Debug.Log("refreshDeploymentsAsync"); // Sanity check requestId - if refreshBtn is enabled, we *should* have it if (string.IsNullOrEmpty(_deploymentRequestId)) { // We must have stale data - reset clearDeploymentConnections(); return; } hideResultLabels(); // clearDeploymentConnections(); // We want to leave the old URL while we only have one _deploymentsConnectionStatusLabel.text = EdgegapWindowMetadata.WrapRichTextInColor( "<i>Refreshing...</i>", EdgegapWindowMetadata.StatusColors.Processing); EdgegapDeploymentsApi deployApi = getDeployApi(); EdgegapHttpResult<GetDeploymentStatusResult> getDeploymentStatusResponse = await deployApi.GetDeploymentStatusAsync(_deploymentRequestId); bool isActiveStatus = getDeploymentStatusResponse?.StatusCode != null && getDeploymentStatusResponse.Data.CurrentStatus == EdgegapWindowMetadata.READY_STATUS; if (isActiveStatus) onCreateDeploymentOrRefreshSuccess(getDeploymentStatusResponse.Data); else { onCreateDeploymentStartServerFail(); if (!getDeploymentStatusResponse.HasErr) onRefreshDeploymentStoppedStatus(); // Only a "soft" fail - not a true err } } /// <summary>The deployment simply stopped (a "soft" fail - not an actual err)</summary> private void onRefreshDeploymentStoppedStatus() { // Override to Create fail -- instead, we've simplfy stopped (a "soft" fail) _deploymentsConnectionStatusLabel.text = getConnectionStoppedRichStr(); _deploymentsStatusLabel.style.display = DisplayStyle.None; _deploymentsConnectionStopBtn.SetEnabled(false); _deploymentsConnectionStopBtn.visible = true; } /// <summary>Don't use this if you want to keep the last-known connection info.</summary> private void clearDeploymentConnections() { _deploymentsConnectionUrlReadonlyInput.value = ""; _deploymentsConnectionStatusLabel.text = ""; _deploymentsConnectionStopBtn.visible = false; _deploymentsRefreshBtn.SetEnabled(false); } /// <summary> /// V2 Successor to legacy startServerCallbackAsync() from "Create New Deployment" Btn. /// </summary> private async Task createDeploymentStartServerAsync() { // Hide previous result labels, disable btns (to reenable when done) if (IsLogLevelDebug) Debug.Log("createDeploymentStartServerAsync"); hideResultLabels(); _deploymentsCreateBtn.SetEnabled(false); _deploymentsRefreshBtn.SetEnabled(false); // _deploymentsConnectionUrlReadonlyInput.value = ""; // We currently want to keep the last known connection, even on err _deploymentsConnectionStatusLabel.text = EdgegapWindowMetadata.WrapRichTextInColor( EdgegapWindowMetadata.DEPLOY_REQUEST_RICH_STR, EdgegapWindowMetadata.StatusColors.Processing); try { EdgegapDeploymentsApi deployApi = getDeployApi(); // Get (+cache) external IP async, required to create a deployment. Prioritize cache. _userExternalIp = await getExternalIpAddress(); CreateDeploymentRequest createDeploymentReq = new CreateDeploymentRequest( // MIRROR CHANGE: 'new()' not supported in Unity 2020 _appNameInput.value, _containerNewTagVersionInput.value, _userExternalIp); // Request to deploy (it won't be active, yet) => EdgegapHttpResult<CreateDeploymentResult> createDeploymentResponse = await deployApi.CreateDeploymentAsync(createDeploymentReq); if (!createDeploymentResponse.IsResultCode200) { onCreateDeploymentStartServerFail(createDeploymentResponse); return; } else { // Update status _deploymentsConnectionStatusLabel.text = EdgegapWindowMetadata.WrapRichTextInColor( "<i>Deploying...</i>", EdgegapWindowMetadata.StatusColors.Processing); } // Check the status of the deployment for READY every 2s => const int pollIntervalSecs = EdgegapWindowMetadata.DEPLOYMENT_READY_STATUS_POLL_SECONDS; EdgegapHttpResult<GetDeploymentStatusResult> getDeploymentStatusResponse = await deployApi.AwaitReadyStatusAsync( createDeploymentResponse.Data.RequestId, TimeSpan.FromSeconds(pollIntervalSecs)); // Process create deployment response bool isSuccess = createDeploymentResponse.IsResultCode200; if (isSuccess) onCreateDeploymentOrRefreshSuccess(getDeploymentStatusResponse.Data); else onCreateDeploymentStartServerFail(createDeploymentResponse); _deploymentsStatusLabel.style.display = DisplayStyle.Flex; } finally { _deploymentsCreateBtn.SetEnabled(true); } } /// <summary> /// CreateDeployment || RefreshDeployment success handler. /// </summary> /// <param name="getDeploymentStatusResult">Only pass from CreateDeployment</param> private void onCreateDeploymentOrRefreshSuccess(GetDeploymentStatusResult getDeploymentStatusResult) { // Success hideResultLabels(); _deploymentsStatusLabel.text = EdgegapWindowMetadata.WrapRichTextInColor( "Success", EdgegapWindowMetadata.StatusColors.Success); _deploymentsStatusLabel.style.display = DisplayStyle.Flex; // Cache the deployment result -> persist the requestId _lastKnownDeployment = getDeploymentStatusResult; _deploymentRequestId = getDeploymentStatusResult.RequestId; EditorPrefs.SetString(EdgegapWindowMetadata.DEPLOYMENT_REQUEST_ID_KEY_STR, _deploymentRequestId); // ------------ // Set the static connection row label data >> // TODO: This will be dynamically inserted via MVC-style template when we support multiple deployments >> // Get external port // BUG(WORKAROUND): Expected `ports` to be List<AppPortsData>, but received Dictionary<string, AppPortsData> KeyValuePair<string, DeploymentPortsData> portsDataKvp = getDeploymentStatusResult.PortsDict.FirstOrDefault(); Assert.IsNotNull(portsDataKvp.Value, $"Expected ({nameof(portsDataKvp)} from `getDeploymentStatusResult.PortsDict`)"); DeploymentPortsData deploymentPortData = portsDataKvp.Value; string externalPortStr = deploymentPortData.External.ToString(); string domainWithExternalPort = $"{getDeploymentStatusResult.Fqdn}:{externalPortStr}"; _deploymentsConnectionUrlReadonlyInput.value = domainWithExternalPort; string newConnectionStatus = EdgegapWindowMetadata.WrapRichTextInColor( "Deployed", EdgegapWindowMetadata.StatusColors.Success); // Change + Persist read-only fields (ViewDataKeys only save automatically from human input) setPersistDeploymentsConnectionUrlLabelTxt(domainWithExternalPort); setPersistDeploymentsConnectionStatusLabelTxt(newConnectionStatus); // ------------ // Configure + show stop button _deploymentsConnectionStopBtn.clickable.clickedWithEventInfo += onDynamicStopServerBtnAsync; // Unsubscribes on click _deploymentsConnectionStopBtn.visible = true; _deploymentsConnectionStopBtn.SetEnabled(true); // Show refresh btn (currently targeting only this one) _deploymentsRefreshBtn.SetEnabled(true); } private void onCreateDeploymentStartServerFail(EdgegapHttpResult<CreateDeploymentResult> result = null) { _deploymentsConnectionStatusLabel.text = EdgegapWindowMetadata.WrapRichTextInColor( "Failed to Start", EdgegapWindowMetadata.StatusColors.Error); _deploymentsStatusLabel.text = EdgegapWindowMetadata.WrapRichTextInColor( result?.Error.ErrorMessage ?? "Unknown Error", EdgegapWindowMetadata.StatusColors.Error); _deploymentsStatusLabel.style.display = DisplayStyle.Flex; _deploymentsRefreshBtn.SetEnabled(true); Debug.Log("(!) Check your deployments here: https://app.edgegap.com/deployment-management/deployments/list"); // Shake "need more servers" btn on 403 // MIRROR CHANGE: use old C# syntax that is supported in Unity 2019 // bool reachedNumDeploymentsHardcap = result is { IsResultCode403: true }; bool reachedNumDeploymentsHardcap = result != null && result.IsResultCode403; // END MIRROR CHANGE if (reachedNumDeploymentsHardcap) shakeNeedMoreGameServersBtn(); } /// <summary> /// This is triggered from a dynamic button, so we need to pass in the event info (TODO: Use evt info later). /// </summary> /// <param name="evt"></param> private void onDynamicStopServerBtnAsync(EventBase evt) => _ = onDynamicStopServerAsync(); /// <summary> /// Stops the deployment, updating the UI accordingly. /// TODO: Cache a list of deployments and/or store a hidden field for requestId. /// </summary> private async Task onDynamicStopServerAsync() { // Prepare to stop (UI, status flags, callback unsubs) if (IsLogLevelDebug) Debug.Log("onDynamicStopServerAsync"); hideResultLabels(); _deploymentsConnectionStopBtn.SetEnabled(false); _deploymentsRefreshBtn.SetEnabled(false); _deploymentsConnectionStatusLabel.text = EdgegapWindowMetadata.WrapRichTextInColor( "<i>Requesting Stop...</i>", EdgegapWindowMetadata.StatusColors.Processing); EdgegapDeploymentsApi deployApi = getDeployApi(); EdgegapHttpResult<StopActiveDeploymentResult> stopResponse = null; try { stopResponse = await deployApi.StopActiveDeploymentAsync(_deploymentRequestId); if (!stopResponse.IsResultCode200) { onDynamicStopServerAsyncFail(stopResponse.Error.ErrorMessage); return; } // --------- // 200, but only PENDING deleted (if we create a new one before it's deleted, // user may get hit with max # of deployments reached err) _deploymentsConnectionStatusLabel.text = EdgegapWindowMetadata.WrapRichTextInColor( "<i>Stopping...</i>", EdgegapWindowMetadata.StatusColors.Warn); TimeSpan pollIntervalSecs = TimeSpan.FromSeconds(EdgegapWindowMetadata.DEPLOYMENT_STOP_STATUS_POLL_SECONDS); stopResponse = await deployApi.AwaitTerminatedDeleteStatusAsync(_deploymentRequestId, pollIntervalSecs); } finally { _deploymentsConnectionStopBtn.clickable.clickedWithEventInfo -= onDynamicStopServerBtnAsync; } bool isStopSuccess = stopResponse.IsResultCode410; if (!isStopSuccess) { onDynamicStopServerAsyncFail(stopResponse.Error.ErrorMessage); return; } // Success: Hide the static row // TODO: Delete the template row, when dynamic // clearDeploymentConnections(); // Use this if you don't want to show the last connection info string stoppedStr = getConnectionStoppedRichStr(); _deploymentsStatusLabel.text = ""; // Overrides any previous errs, in case we attempted to created a new deployment while deleting setPersistDeploymentsConnectionStatusLabelTxt(stoppedStr); } private string getConnectionStoppedRichStr() => EdgegapWindowMetadata.WrapRichTextInColor( "Stopped", EdgegapWindowMetadata.StatusColors.Error); private void onDynamicStopServerAsyncFail(string friendlyErrMsg) { _deploymentsStatusLabel.text = EdgegapWindowMetadata.WrapRichTextInColor( friendlyErrMsg, EdgegapWindowMetadata.StatusColors.Error); } /// <summary>Sets and returns `_userExternalIp`, prioritizing local cache</summary> private async Task<string> getExternalIpAddress() { if (!string.IsNullOrEmpty(_userExternalIp)) return _userExternalIp; EdgegapIpApi ipApi = getIpApi(); EdgegapHttpResult<GetYourPublicIpResult> getYourPublicIpResponseTask = await ipApi.GetYourPublicIp(); _userExternalIp = getYourPublicIpResponseTask?.Data?.PublicIp; Assert.IsTrue(!string.IsNullOrEmpty(_userExternalIp), $"Expected getYourPublicIpResponseTask.Data.PublicIp"); return _userExternalIp; } #region Api Builders private EdgegapDeploymentsApi getDeployApi() => new EdgegapDeploymentsApi( // MIRROR CHANGE: 'new()' not supported in Unity 2020 EdgegapWindowMetadata.API_ENVIRONMENT, _apiTokenInput.value.Trim(), EdgegapWindowMetadata.LOG_LEVEL); private EdgegapIpApi getIpApi() => new EdgegapIpApi( // MIRROR CHANGE: 'new()' not supported in Unity 2020 EdgegapWindowMetadata.API_ENVIRONMENT, _apiTokenInput.value.Trim(), EdgegapWindowMetadata.LOG_LEVEL); private EdgegapWizardApi getWizardApi() => new EdgegapWizardApi( // MIRROR CHANGE: 'new()' not supported in Unity 2020 EdgegapWindowMetadata.API_ENVIRONMENT, _apiTokenInput.value.Trim(), EdgegapWindowMetadata.LOG_LEVEL); private EdgegapAppApi getAppApi() => new EdgegapAppApi( // MIRROR CHANGE: 'new()' not supported in Unity 2020 EdgegapWindowMetadata.API_ENVIRONMENT, _apiTokenInput.value.Trim(), EdgegapWindowMetadata.LOG_LEVEL); #endregion // Api Builders private float ProgressCounter = 0; // MIRROR CHANGE: added title parameter for more detailed progress while waiting void ShowBuildWorkInProgress(string title, string status) { EditorUtility.DisplayProgressBar(title, status, ProgressCounter++ / 50); } // END MIRROR CHANGE /// <summary>Build & Push - Legacy from v1, modified for v2</summary> private async Task buildAndPushServerAsync() { if (IsLogLevelDebug) Debug.Log("buildAndPushServerAsync"); // Legacy Code Start >> // SetToolUIState(ToolState.Building); SyncObjectWithForm(); ProgressCounter = 0; try { // check for installation and setup docker file if (!await EdgegapBuildUtils.DockerSetupAndInstallationCheck()) { onBuildPushError("Docker installation not found. " + "Docker can be downloaded from:\n\nhttps://www.docker.com/"); return; } // MIRROR CHANGE // make sure Linux build target is installed before attemping to build. // if it's not installed, tell the user about it. if (!BuildPipeline.IsBuildTargetSupported(BuildTargetGroup.Standalone, BuildTarget.StandaloneLinux64)) { onBuildPushError($"Linux Build Support is missing.\n\nPlease open Unity Hub -> Installs -> Unity {Application.unityVersion} -> Add Modules -> Linux Build Support (IL2CPP & Mono & Dedicated Server) -> Install\n\nAfterwards restart Unity!"); return; } // END MIRROR CHANGE if (!EdgegapWindowMetadata.SKIP_SERVER_BUILD_WHEN_PUSHING) { // create server build BuildReport buildResult = EdgegapBuildUtils.BuildServer(); if (buildResult.summary.result != UnityEditor.Build.Reporting.BuildResult.Succeeded) { onBuildPushError("Edgegap build failed"); return; } } else Debug.LogWarning(nameof(EdgegapWindowMetadata.SKIP_SERVER_BUILD_WHEN_PUSHING)); string registry = _containerRegistryUrlInput.value; string imageName = _containerImageRepositoryInput.value; string tag = _containerNewTagVersionInput.value; // MIRROR CHANGE /////////////////////////////////////////////// // registry, repository and tag can not contain whitespaces. // otherwise the docker command will throw an error: // "ERROR: "docker buildx build" requires exactly 1 argument." // catch this early and notify the user immediately. if (registry.Contains(" ")) { onBuildPushError($"Container Registry is not allowed to contain whitespace: '{registry}'"); return; } if (imageName.Contains(" ")) { onBuildPushError($"Image Repository is not allowed to contain whitespace: '{imageName}'"); return; } if (tag.Contains(" ")) { onBuildPushError($"Tag is not allowed to contain whitespace: '{tag}'"); return; } // END MIRROR CHANGE /////////////////////////////////////////// // // increment tag for quicker iteration // TODO? `_autoIncrementTag` !exists in V2. // if (_autoIncrementTag) // { // tag = EdgegapBuildUtils.IncrementTag(tag); // } // create docker image if (!EdgegapWindowMetadata.SKIP_DOCKER_IMAGE_BUILD_WHEN_PUSHING) { // MIRROR CHANGE: CROSS PLATFORM BUILD SUPPORT // await EdgegapBuildUtils.DockerBuild( // registry, // imageName, // tag, // ShowBuildWorkInProgress); await EdgegapBuildUtils.RunCommand_DockerBuild(registry, imageName, tag, status => ShowBuildWorkInProgress("Building Docker Image", status)); } else Debug.LogWarning(nameof(EdgegapWindowMetadata.SKIP_DOCKER_IMAGE_BUILD_WHEN_PUSHING)); // (v2) Login to registry bool isContainerLoginSuccess = await EdgegapBuildUtils.LoginContainerRegistry( _containerRegistryUrlInput.value, _containerUsernameInput.value, _containerTokenInput.value, status => ShowBuildWorkInProgress("Logging into container registry.", status)); // MIRROR CHANGE: DETAILED LOGGING if (!isContainerLoginSuccess) { onBuildPushError("Unable to login to docker registry. " + "Make sure your registry url + username are correct. " + $"See doc:\n\n{EdgegapWindowMetadata.EDGEGAP_DOC_BTN_HOW_TO_LOGIN_VIA_CLI_URL}"); return; } // MIRROR CHANGE: DETAILED DOCKER PUSH ERROR HANDLING // push docker image (bool isPushSuccess, string error) = await EdgegapBuildUtils.RunCommand_DockerPush(registry, imageName, tag, status => ShowBuildWorkInProgress("Uploading Docker Image (this may take a while)", status)); if (!isPushSuccess) { // catch common issues with detailed solutions if (error.Contains("Cannot connect to the Docker daemon")) { onBuildPushError($"{error}\nTo solve this, you can install and run Docker Desktop from:\n\nhttps://www.docker.com/products/docker-desktop"); return; } if (error.Contains("unauthorized to access repository")) { onBuildPushError($"Docker authorization failed:\n\n{error}\nTo solve this, you can open a terminal and enter 'docker login {registry}', then enter your credentials."); return; } // project not found? if (Regex.IsMatch(error, @".*project .* not found.*", RegexOptions.IgnoreCase)) { onBuildPushError($"{error}\nTo solve this, make sure that Image Repository is 'project/game' where 'project' is from the Container Registry page on the Edgegap website."); return; } // otherwise show generic error message onBuildPushError("Unable to push docker image to registry. " + $"Make sure your {registry} registry url + username are correct. " + $"See doc:\n\n{EdgegapWindowMetadata.EDGEGAP_DOC_BTN_HOW_TO_LOGIN_VIA_CLI_URL}"); return; } // END MIRROR CHANGE // update edgegap server settings for new tag ShowBuildWorkInProgress("Build and Push", "Updating server info on Edgegap"); EdgegapAppApi appApi = getAppApi(); AppPortsData[] ports = { new AppPortsData() // MIRROR CHANGE: 'new()' not supported in Unity 2020 { Port = int.Parse(_containerPortNumInput.value), // OnInputChange clamps + validates, ProtocolStr = _containerTransportTypeEnumInput.value.ToString(), TlsUpgrade = _containerTransportTypeEnumInput.value.ToString() == ProtocolType.WS.ToString() // If the protocol is WebSocket, we seemlessly add tls_upgrade. If we want to add it to other protocols, we need to change this. }, }; UpdateAppVersionRequest updateAppVerReq = new UpdateAppVersionRequest(_appNameInput.value) // MIRROR CHANGE: 'new()' not supported in Unity 2020 { VersionName = _containerNewTagVersionInput.value, DockerImage = imageName, DockerRepository = registry, DockerTag = tag, PrivateUsername = _containerUsernameInput.value, PrivateToken = _containerTokenInput.value, Ports = ports, }; EdgegapHttpResult<UpsertAppVersionResult> updateAppVersionResult = await appApi.UpsertAppVersion(updateAppVerReq); if (updateAppVersionResult.HasErr) { onBuildPushError($"Unable to update docker tag/version:\n{updateAppVersionResult.Error.ErrorMessage}"); return; } // cleanup onBuildAndPushSuccess(tag); } catch (Exception ex) { EditorUtility.ClearProgressBar(); Debug.LogError(ex); string errMsg = "Edgegapbuild and push failed"; if (ex.Message.Contains("docker daemon is not running")) { errMsg += ":\nDocker is installed, but the daemon/app (such as `Docker Desktop`) is not running. " + "Please start Docker Desktop and try again."; } else errMsg += $":\n{ex.Message}"; onBuildPushError(errMsg); } // MIRROR CHANGE: always clear otherwise it gets stuck there forever! finally { EditorUtility.ClearProgressBar(); } // END MIRROR CHANGE } private void onBuildAndPushSuccess(string tag) { // _containerImageTag = tag; // TODO? syncFormWithObjectStatic(); EditorUtility.ClearProgressBar(); _containerBuildAndPushResultLabel.text = $"Success ({tag})"; _containerBuildAndPushResultLabel.visible = true; Debug.Log("Server built and pushed successfully"); } /// <summary>(v2) Docker cmd error, detected by "ERROR" in log stream.</summary> private void onBuildPushError(string msg) { EditorUtility.ClearProgressBar(); _containerBuildAndPushResultLabel.text = EdgegapWindowMetadata.WrapRichTextInColor( "Error", EdgegapWindowMetadata.StatusColors.Error); EditorUtility.DisplayDialog("Error", msg, "Ok"); // Show this last! It's blocking! } #region Persistence Helpers /// <summary> /// Load from EditorPrefs, persisting from a previous session, if the field is empty /// - ApiToken; !persisted via ViewDataKey so we don't save plaintext /// - DeploymentRequestId /// - DeploymentConnectionUrl /// - DeploymentConnectionStatus /// </summary> private void loadPersistentDataFromEditorPrefs() { // ApiToken if (string.IsNullOrEmpty(_apiTokenInput.value)) setMaskedApiTokenFromEditorPrefs(); // DeploymentRequestId if (string.IsNullOrEmpty(_deploymentRequestId)) _deploymentRequestId = EditorPrefs.GetString(EdgegapWindowMetadata.DEPLOYMENT_REQUEST_ID_KEY_STR); // DeploymentConnectionUrl if (string.IsNullOrEmpty(_deploymentsConnectionUrlReadonlyInput.text)) { _deploymentsConnectionUrlReadonlyInput.value = getDeploymentsConnectionUrlLabelTxt(); bool hasVal = !string.IsNullOrEmpty(_deploymentsConnectionUrlReadonlyInput.value); if (hasVal && string.IsNullOrEmpty(_deploymentRequestId)) { // Fallback -- if no requestId, we can actually get it from the url since we have this (desync) _deploymentRequestId = _deploymentsConnectionUrlReadonlyInput.value.Split('.')[0]; EditorPrefs.SetString(EdgegapWindowMetadata.DEPLOYMENT_REQUEST_ID_KEY_STR, _deploymentRequestId); } // TODO (Optional): Show a status label to remind these are cached vals; refresh for live? } // DeploymentConnectionStatus if (string.IsNullOrEmpty(_deploymentsConnectionStatusLabel.text) || _deploymentsConnectionStatusLabel.text == "Unknown") _deploymentsConnectionStatusLabel.text = getDeploymentsConnectionStatusLabelTxt(); } /// <summary>Set Label -> Persist to EditorPrefs</summary> /// <param name="newDomainWithPort"></param> private void setPersistDeploymentsConnectionUrlLabelTxt(string newDomainWithPort) { _deploymentsConnectionUrlReadonlyInput.value = newDomainWithPort; EditorPrefs.SetString(EdgegapWindowMetadata.DEPLOYMENT_CONNECTION_URL_KEY_STR, newDomainWithPort); } /// <summary>Get persistent data fromEditorPrefs</summary> private string getDeploymentsConnectionUrlLabelTxt() => EditorPrefs.GetString(EdgegapWindowMetadata.DEPLOYMENT_CONNECTION_URL_KEY_STR); /// <summary>Set label -> persist to EditorPrefs</summary> /// <param name="newStatus"></param> private void setPersistDeploymentsConnectionStatusLabelTxt(string newStatus) { _deploymentsConnectionStatusLabel.text = newStatus; EditorPrefs.SetString(EdgegapWindowMetadata.DEPLOYMENT_CONNECTION_STATUS_KEY_STR, newStatus); } /// <summary>Get persistent data fromEditorPrefs</summary> private string getDeploymentsConnectionStatusLabelTxt() => EditorPrefs.GetString(EdgegapWindowMetadata.DEPLOYMENT_CONNECTION_STATUS_KEY_STR); /// <summary>Set to base64 -> Save to EditorPrefs</summary> /// <param name="value"></param> private void persistUnmaskedApiToken(string value) { EditorPrefs.SetString( EdgegapWindowMetadata.API_TOKEN_KEY_STR, Base64Encode(value)); } /// <summary> /// Get apiToken from EditorPrefs -> Base64 Decode -> Set to apiTokenInput /// </summary> private void setMaskedApiTokenFromEditorPrefs() { string apiTokenBase64Str = EditorPrefs.GetString( EdgegapWindowMetadata.API_TOKEN_KEY_STR, null); if (apiTokenBase64Str == null) return; string decodedApiToken = Base64Decode(apiTokenBase64Str); _apiTokenInput.SetValueWithoutNotify(decodedApiToken); } #endregion // Persistence Helpers } }