1768 lines
84 KiB
C#
1768 lines
84 KiB
C#
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
|
|
}
|
|
}
|