EdgegapWindowV2.cs 83 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766
  1. using System;
  2. using System.Collections.Generic;
  3. using System.IO;
  4. using System.Linq;
  5. using System.Net.Http;
  6. using System.Text;
  7. using System.Text.RegularExpressions;
  8. using System.Threading.Tasks;
  9. using Edgegap.Editor.Api;
  10. using Edgegap.Editor.Api.Models;
  11. using Edgegap.Editor.Api.Models.Requests;
  12. using Edgegap.Editor.Api.Models.Results;
  13. using UnityEditor;
  14. using UnityEditor.Build.Reporting;
  15. using UnityEditor.UIElements;
  16. using UnityEngine;
  17. using UnityEngine.Assertions;
  18. using UnityEngine.UIElements;
  19. using Application = UnityEngine.Application;
  20. namespace Edgegap.Editor
  21. {
  22. /// <summary>
  23. /// Editor logic event handler for "UI Builder" EdgegapWindow.uxml, superceding` EdgegapWindow.cs`.
  24. /// </summary>
  25. public class EdgegapWindowV2 : EditorWindow
  26. {
  27. #region Vars
  28. public static bool IsLogLevelDebug =>
  29. EdgegapWindowMetadata.LOG_LEVEL == EdgegapWindowMetadata.LogLevel.Debug;
  30. private bool IsInitd;
  31. private VisualTreeAsset _visualTree;
  32. private bool _isApiTokenVerified; // Toggles the rest of the UI
  33. private bool _isContainerRegistryReady;
  34. private Sprite _appIconSpriteObj;
  35. private string _appIconBase64Str;
  36. #pragma warning disable CS0414 // MIRROR CHANGE: hide unused warning
  37. private ApiEnvironment _apiEnvironment; // TODO: Swap out hard-coding with UI element?
  38. #pragma warning restore CS0414 // END MIRROR CHANGE
  39. private GetRegistryCredentialsResult _credentials;
  40. private static readonly Regex _appNameAllowedCharsRegex = new Regex(@"^[a-zA-Z0-9_\-+\.]*$"); // MIRROR CHANGE: 'new()' not supported in Unity 2020
  41. private GetCreateAppResult _loadedApp;
  42. /// <summary>TODO: Make this a list</summary>
  43. private GetDeploymentStatusResult _lastKnownDeployment;
  44. private string _deploymentRequestId;
  45. private string _userExternalIp;
  46. private bool _isAwaitingDeploymentReadyStatus;
  47. #endregion // Vars
  48. #region Vars -> Interactable Elements
  49. private Button _debugBtn;
  50. /// <summary>(!) This is saved manually to EditorPrefs via Base64 instead of via UiBuilder</summary>
  51. private TextField _apiTokenInput;
  52. private Button _apiTokenVerifyBtn;
  53. private Button _apiTokenGetBtn;
  54. private VisualElement _postAuthContainer;
  55. private Foldout _appInfoFoldout;
  56. private Button _appLoadExistingBtn;
  57. private TextField _appNameInput;
  58. /// <summary>`Sprite` type</summary>
  59. private ObjectField _appIconSpriteObjInput;
  60. private Button _appCreateBtn;
  61. private Label _appCreateResultLabel;
  62. private Foldout _containerRegistryFoldout;
  63. private TextField _containerNewTagVersionInput;
  64. private TextField _containerPortNumInput;
  65. // MIRROR CHANGE: EnumField Port type fails to resolve unless in Assembly-CSharp-Editor.dll. replace with regular Dropdown instead.
  66. /// <summary>`ProtocolType` type</summary>
  67. // private EnumField _containerTransportTypeEnumInput;
  68. private PopupField<string> _containerTransportTypeEnumInput;
  69. // END MIRROR CHANGE
  70. private Toggle _containerUseCustomRegistryToggle;
  71. private VisualElement _containerCustomRegistryWrapper;
  72. private TextField _containerRegistryUrlInput;
  73. private TextField _containerImageRepositoryInput;
  74. private TextField _containerUsernameInput;
  75. private TextField _containerTokenInput;
  76. private Button _containerBuildAndPushServerBtn;
  77. private Label _containerBuildAndPushResultLabel;
  78. private Foldout _deploymentsFoldout;
  79. private Button _deploymentsRefreshBtn;
  80. private Button _deploymentsCreateBtn;
  81. /// <summary>display:none (since it's on its own line), rather than !visible.</summary>
  82. private Label _deploymentsStatusLabel;
  83. private VisualElement _deploymentsServerDataContainer;
  84. private Button _deploymentConnectionCopyUrlBtn;
  85. private TextField _deploymentsConnectionUrlReadonlyInput;
  86. private Label _deploymentsConnectionStatusLabel;
  87. private Button _deploymentsConnectionStopBtn;
  88. private Button _footerDocumentationBtn;
  89. private Button _footerNeedMoreGameServersBtn;
  90. #endregion // Vars
  91. // MIRROR CHANGE
  92. // get the path of this .cs file so we don't need to hardcode paths to
  93. // the .uxml and .uss files:
  94. // https://forum.unity.com/threads/too-many-hard-coded-paths-in-the-templates-and-documentation.728138/
  95. // this way users can move this folder without breaking UIToolkit paths.
  96. internal string StylesheetPath =>
  97. Path.GetDirectoryName(AssetDatabase.GetAssetPath(MonoScript.FromScriptableObject(this)));
  98. // END MIRROR CHANGE
  99. // 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.
  100. public Texture2D LogoImage;
  101. public Texture2D ClipboardImage;
  102. // END MIRROR CHANGE
  103. [MenuItem("Edgegap/Edgegap Hosting")] // MIRROR CHANGE: more obvious title
  104. public static void ShowEdgegapToolWindow()
  105. {
  106. EdgegapWindowV2 window = GetWindow<EdgegapWindowV2>();
  107. window.titleContent = new GUIContent("Edgegap Hosting"); // MIRROR CHANGE: 'Edgegap Server Management' is too long for the tab space
  108. window.maxSize = new Vector2(635, 900);
  109. window.minSize = window.maxSize;
  110. }
  111. #region Unity Funcs
  112. protected void OnEnable()
  113. {
  114. #if UNITY_2021_3_OR_NEWER // MIRROR CHANGE: only load stylesheet in supported Unity versions, otherwise it shows errors in U2020
  115. // Set root VisualElement and style: V2 still uses EdgegapWindow.[uxml|uss]
  116. // BEGIN MIRROR CHANGE
  117. _visualTree = AssetDatabase.LoadAssetAtPath<VisualTreeAsset>($"{StylesheetPath}/EdgegapWindow.uxml");
  118. StyleSheet styleSheet = AssetDatabase.LoadAssetAtPath<StyleSheet>($"{StylesheetPath}/EdgegapWindow.uss");
  119. // END MIRROR CHANGE
  120. rootVisualElement.styleSheets.Add(styleSheet);
  121. #endif
  122. }
  123. #pragma warning disable CS1998 // MIRROR CHANGE: disable async warning in U2020
  124. public async void CreateGUI()
  125. #pragma warning restore CS1998 // END MIRROR CHANGE
  126. {
  127. // MIRROR CHANGE: the UI requires 'GroupBox', which is not available in Unity 2019/2020.
  128. // showing it will break all of Unity's Editor UIs, not just this one.
  129. // instead, show a warning that the Edgegap plugin only works on Unity 2021+
  130. #if !UNITY_2021_3_OR_NEWER
  131. Debug.LogWarning("The Edgegap Hosting plugin requires UIToolkit in Unity 2021.3 or newer. Please upgrade your Unity version to use this.");
  132. #else
  133. // Get UI elements from UI Builder
  134. rootVisualElement.Clear();
  135. _visualTree.CloneTree(rootVisualElement);
  136. // Register callbacks and sync UI builder elements to fields here
  137. InitUIElements();
  138. syncFormWithObjectStatic();
  139. await syncFormWithObjectDynamicAsync(); // API calls
  140. IsInitd = true;
  141. #endif
  142. }
  143. /// <summary>The user closed the window. Save the data.</summary>
  144. protected void OnDisable()
  145. {
  146. #if UNITY_2021_3_OR_NEWER // MIRROR CHANGE: only load stylesheet in supported Unity versions, otherwise it shows errors in U2020
  147. // MIRROR CHANGE: sometimes this is called without having been registered, throwing NRE
  148. if (_debugBtn == null) return;
  149. // END MIRROR CHANGE
  150. unregisterClickEvents();
  151. unregisterFieldCallbacks();
  152. SyncObjectWithForm();
  153. #endif
  154. }
  155. #endregion // Unity Funcs
  156. #region Init
  157. /// <summary>
  158. /// Binds the form inputs to the associated variables and initializes the inputs as required.
  159. /// Requires the VisualElements to be loaded before this call. Otherwise, the elements cannot be found.
  160. /// </summary>
  161. private void InitUIElements()
  162. {
  163. setVisualElementsToFields();
  164. assertVisualElementKeys();
  165. closeDisableGroups();
  166. registerClickCallbacks();
  167. registerFieldCallbacks();
  168. initToggleDynamicUi();
  169. AssignImages(); // MIRROR CHANGE
  170. }
  171. private void closeDisableGroups()
  172. {
  173. _appInfoFoldout.value = false;
  174. _containerRegistryFoldout.value = false;
  175. _deploymentsFoldout.value = false;
  176. _appInfoFoldout.SetEnabled(false);
  177. _containerRegistryFoldout.SetEnabled(false);
  178. _deploymentsFoldout.SetEnabled(false);
  179. }
  180. // MIRROR CHANGE: assign images to the UI at runtime instead of hardcoding it
  181. void AssignImages()
  182. {
  183. // header logo
  184. VisualElement logoElement = rootVisualElement.Q<VisualElement>("header-logo-img");
  185. logoElement.style.backgroundImage = LogoImage;
  186. // clipboard button
  187. VisualElement copyElement = rootVisualElement.Q<VisualElement>("DeploymentConnectionCopyUrlBtn");
  188. copyElement.style.backgroundImage = ClipboardImage;
  189. }
  190. // END MIRROR CHANGE
  191. /// <summary>Set fields referencing UI Builder's fields. In order of appearance from top-to-bottom.</summary>
  192. private void setVisualElementsToFields()
  193. {
  194. _debugBtn = rootVisualElement.Q<Button>(EdgegapWindowMetadata.DEBUG_BTN_ID);
  195. _apiTokenInput = rootVisualElement.Q<TextField>(EdgegapWindowMetadata.API_TOKEN_TXT_ID);
  196. _apiTokenVerifyBtn = rootVisualElement.Q<Button>(EdgegapWindowMetadata.API_TOKEN_VERIFY_BTN_ID);
  197. _apiTokenGetBtn = rootVisualElement.Q<Button>(EdgegapWindowMetadata.API_TOKEN_GET_BTN_ID);
  198. _postAuthContainer = rootVisualElement.Q<VisualElement>(EdgegapWindowMetadata.POST_AUTH_CONTAINER_ID);
  199. _appInfoFoldout = rootVisualElement.Q<Foldout>(EdgegapWindowMetadata.APP_INFO_FOLDOUT_ID);
  200. _appNameInput = rootVisualElement.Q<TextField>(EdgegapWindowMetadata.APP_NAME_TXT_ID);
  201. _appLoadExistingBtn = rootVisualElement.Q<Button>(EdgegapWindowMetadata.APP_LOAD_EXISTING_BTN_ID);
  202. _appIconSpriteObjInput = rootVisualElement.Q<ObjectField>(EdgegapWindowMetadata.APP_ICON_SPRITE_OBJ_ID);
  203. _appCreateBtn = rootVisualElement.Q<Button>(EdgegapWindowMetadata.APP_CREATE_BTN_ID);
  204. _appCreateResultLabel = rootVisualElement.Q<Label>(EdgegapWindowMetadata.APP_CREATE_RESULT_LABEL_ID);
  205. _containerRegistryFoldout = rootVisualElement.Q<Foldout>(EdgegapWindowMetadata.CONTAINER_REGISTRY_FOLDOUT_ID);
  206. _containerNewTagVersionInput = rootVisualElement.Q<TextField>(EdgegapWindowMetadata.CONTAINER_NEW_TAG_VERSION_TXT_ID);
  207. _containerPortNumInput = rootVisualElement.Q<TextField>(EdgegapWindowMetadata.CONTAINER_REGISTRY_PORT_NUM_ID);
  208. // MIRROR CHANGE: dynamically resolving PortType fails if not in Assembly-CSharp-Editor.dll. Hardcode UDP/TCP instead.
  209. // this finds the placeholder and dynamically replaces it with a popup field
  210. VisualElement dropdownPlaceholder = rootVisualElement.Q<VisualElement>("MIRROR_CHANGE_PORT_HARDCODED");
  211. List<string> options = new List<string> { "UDP", "TCP" };
  212. _containerTransportTypeEnumInput = new PopupField<string>("Protocol Type", options, 0);
  213. dropdownPlaceholder.Add(_containerTransportTypeEnumInput);
  214. // END MIRROR CHANGE
  215. _containerUseCustomRegistryToggle = rootVisualElement.Q<Toggle>(EdgegapWindowMetadata.CONTAINER_USE_CUSTOM_REGISTRY_TOGGLE_ID);
  216. _containerCustomRegistryWrapper = rootVisualElement.Q<VisualElement>(EdgegapWindowMetadata.CONTAINER_CUSTOM_REGISTRY_WRAPPER_ID);
  217. _containerRegistryUrlInput = rootVisualElement.Q<TextField>(EdgegapWindowMetadata.CONTAINER_REGISTRY_URL_TXT_ID);
  218. _containerImageRepositoryInput = rootVisualElement.Q<TextField>(EdgegapWindowMetadata.CONTAINER_IMAGE_REPOSITORY_URL_TXT_ID);
  219. _containerUsernameInput = rootVisualElement.Q<TextField>(EdgegapWindowMetadata.CONTAINER_USERNAME_TXT_ID);
  220. _containerTokenInput = rootVisualElement.Q<TextField>(EdgegapWindowMetadata.CONTAINER_TOKEN_TXT_ID);
  221. _containerBuildAndPushServerBtn = rootVisualElement.Q<Button>(EdgegapWindowMetadata.CONTAINER_BUILD_AND_PUSH_BTN_ID);
  222. _containerBuildAndPushResultLabel = rootVisualElement.Q<Label>(EdgegapWindowMetadata.CONTAINER_BUILD_AND_PUSH_RESULT_LABEL_ID);
  223. _deploymentsFoldout = rootVisualElement.Q<Foldout>(EdgegapWindowMetadata.DEPLOYMENTS_FOLDOUT_ID);
  224. _deploymentsRefreshBtn = rootVisualElement.Q<Button>(EdgegapWindowMetadata.DEPLOYMENTS_REFRESH_BTN_ID);
  225. _deploymentsCreateBtn = rootVisualElement.Q<Button>(EdgegapWindowMetadata.DEPLOYMENTS_CREATE_BTN_ID);
  226. _deploymentsStatusLabel = rootVisualElement.Q<Label>(EdgegapWindowMetadata.DEPLOYMENTS_STATUS_LABEL_ID);
  227. _deploymentsServerDataContainer = rootVisualElement.Q<VisualElement>(EdgegapWindowMetadata.DEPLOYMENTS_CONTAINER_ID); // Dynamic
  228. _deploymentConnectionCopyUrlBtn = rootVisualElement.Q<Button>(EdgegapWindowMetadata.DEPLOYMENTS_CONNECTION_COPY_URL_BTN_ID);
  229. _deploymentsConnectionUrlReadonlyInput = rootVisualElement.Q<TextField>(EdgegapWindowMetadata.DEPLOYMENTS_CONNECTION_URL_READONLY_TXT_ID);
  230. _deploymentsConnectionStatusLabel = rootVisualElement.Q<Label>(EdgegapWindowMetadata.DEPLOYMENTS_CONNECTION_STATUS_LABEL_ID);
  231. _deploymentsConnectionStopBtn = rootVisualElement.Q<Button>(EdgegapWindowMetadata.DEPLOYMENTS_CONNECTION_SERVER_ACTION_STOP_BTN_ID);
  232. _footerDocumentationBtn = rootVisualElement.Q<Button>(EdgegapWindowMetadata.FOOTER_DOCUMENTATION_BTN_ID);
  233. _footerNeedMoreGameServersBtn = rootVisualElement.Q<Button>(EdgegapWindowMetadata.FOOTER_NEED_MORE_GAME_SERVERS_BTN_ID);
  234. _apiEnvironment = EdgegapWindowMetadata.API_ENVIRONMENT; // (!) TODO: Hard-coded while unused in UI
  235. }
  236. /// <summary>
  237. /// Sanity check: If we implicitly changed an #Id, we need to know early so we can update the const.
  238. /// In order of appearance seen in setVisualElementsToFields().
  239. /// </summary>
  240. private void assertVisualElementKeys()
  241. {
  242. // MIRROR CHANGE: this doesn't compile in Unity 2019
  243. /*
  244. try
  245. {
  246. Assert.IsTrue(_apiTokenInput is { name: EdgegapWindowMetadata.API_TOKEN_TXT_ID },
  247. $"Expected {nameof(_apiTokenInput)} via #{EdgegapWindowMetadata.API_TOKEN_TXT_ID}");
  248. Assert.IsTrue(_apiTokenVerifyBtn is { name: EdgegapWindowMetadata.API_TOKEN_VERIFY_BTN_ID },
  249. $"Expected {nameof(_apiTokenVerifyBtn)} via #{EdgegapWindowMetadata.API_TOKEN_VERIFY_BTN_ID}");
  250. Assert.IsTrue(_apiTokenGetBtn is { name: EdgegapWindowMetadata.API_TOKEN_GET_BTN_ID },
  251. $"Expected {nameof(_apiTokenGetBtn)} via #{EdgegapWindowMetadata.API_TOKEN_GET_BTN_ID}");
  252. Assert.IsTrue(_postAuthContainer is { name: EdgegapWindowMetadata.POST_AUTH_CONTAINER_ID },
  253. $"Expected {nameof(_postAuthContainer)} via #{EdgegapWindowMetadata.POST_AUTH_CONTAINER_ID}");
  254. Assert.IsTrue(_appInfoFoldout is { name: EdgegapWindowMetadata.APP_INFO_FOLDOUT_ID },
  255. $"Expected {nameof(_appInfoFoldout)} via #{EdgegapWindowMetadata.APP_INFO_FOLDOUT_ID}");
  256. Assert.IsTrue(_appNameInput is { name: EdgegapWindowMetadata.APP_NAME_TXT_ID },
  257. $"Expected {nameof(_appNameInput)} via #{EdgegapWindowMetadata.APP_NAME_TXT_ID}");
  258. Assert.IsTrue(_appLoadExistingBtn is { name: EdgegapWindowMetadata.APP_LOAD_EXISTING_BTN_ID },
  259. $"Expected {nameof(_appLoadExistingBtn)} via #{EdgegapWindowMetadata.APP_LOAD_EXISTING_BTN_ID}");
  260. Assert.IsTrue(_appIconSpriteObjInput is { name: EdgegapWindowMetadata.APP_ICON_SPRITE_OBJ_ID },
  261. $"Expected {nameof(_appIconSpriteObjInput)} via #{EdgegapWindowMetadata.APP_ICON_SPRITE_OBJ_ID}");
  262. Assert.IsTrue(_appCreateBtn is { name: EdgegapWindowMetadata.APP_CREATE_BTN_ID },
  263. $"Expected {nameof(_appCreateBtn)} via #{EdgegapWindowMetadata.APP_CREATE_BTN_ID}");
  264. Assert.IsTrue(_appCreateResultLabel is { name: EdgegapWindowMetadata.APP_CREATE_RESULT_LABEL_ID },
  265. $"Expected {nameof(_appCreateResultLabel)} via #{EdgegapWindowMetadata.APP_CREATE_RESULT_LABEL_ID}");
  266. Assert.IsTrue(_containerRegistryFoldout is { name: EdgegapWindowMetadata.CONTAINER_REGISTRY_FOLDOUT_ID },
  267. $"Expected {nameof(_containerRegistryFoldout)} via #{EdgegapWindowMetadata.CONTAINER_REGISTRY_FOLDOUT_ID}");
  268. Assert.IsTrue(_containerPortNumInput is { name: EdgegapWindowMetadata.CONTAINER_REGISTRY_PORT_NUM_ID },
  269. $"Expected {nameof(_containerPortNumInput)} via #{EdgegapWindowMetadata.CONTAINER_REGISTRY_PORT_NUM_ID}");
  270. // MIRROR CHANGE: disable and replaced with hardcoded port type dropdown
  271. // Assert.IsTrue(_containerTransportTypeEnumInput is { name: EdgegapWindowMetadata.CONTAINER_REGISTRY_TRANSPORT_TYPE_ENUM_ID },
  272. // $"Expected {nameof(_containerTransportTypeEnumInput)} via #{EdgegapWindowMetadata.CONTAINER_REGISTRY_TRANSPORT_TYPE_ENUM_ID}");
  273. // END MIRROR CHANGE
  274. Assert.IsTrue(_containerUseCustomRegistryToggle is { name: EdgegapWindowMetadata.CONTAINER_USE_CUSTOM_REGISTRY_TOGGLE_ID },
  275. $"Expected {nameof(_containerUseCustomRegistryToggle)} via #{EdgegapWindowMetadata.CONTAINER_USE_CUSTOM_REGISTRY_TOGGLE_ID}");
  276. Assert.IsTrue(_containerCustomRegistryWrapper is { name: EdgegapWindowMetadata.CONTAINER_CUSTOM_REGISTRY_WRAPPER_ID },
  277. $"Expected {nameof(_containerCustomRegistryWrapper)} via #{EdgegapWindowMetadata.CONTAINER_CUSTOM_REGISTRY_WRAPPER_ID}");
  278. Assert.IsTrue(_containerRegistryUrlInput is { name: EdgegapWindowMetadata.CONTAINER_REGISTRY_URL_TXT_ID },
  279. $"Expected {nameof(_containerRegistryUrlInput)} via #{EdgegapWindowMetadata.CONTAINER_REGISTRY_URL_TXT_ID}");
  280. Assert.IsTrue(_containerImageRepositoryInput is { name: EdgegapWindowMetadata.CONTAINER_IMAGE_REPOSITORY_URL_TXT_ID },
  281. $"Expected {nameof(_containerImageRepositoryInput)} via #{EdgegapWindowMetadata.CONTAINER_IMAGE_REPOSITORY_URL_TXT_ID}");
  282. Assert.IsTrue(_containerUsernameInput is { name: EdgegapWindowMetadata.CONTAINER_USERNAME_TXT_ID },
  283. $"Expected {nameof(_containerUsernameInput)} via #{EdgegapWindowMetadata.CONTAINER_USERNAME_TXT_ID}");
  284. Assert.IsTrue(_containerTokenInput is { name: EdgegapWindowMetadata.CONTAINER_TOKEN_TXT_ID },
  285. $"Expected {nameof(_containerTokenInput)} via #{EdgegapWindowMetadata.CONTAINER_TOKEN_TXT_ID}");
  286. Assert.IsTrue(_containerTokenInput is { name: EdgegapWindowMetadata.CONTAINER_TOKEN_TXT_ID },
  287. $"Expected {nameof(_containerTokenInput)} via #{EdgegapWindowMetadata.CONTAINER_TOKEN_TXT_ID}");
  288. Assert.IsTrue(_containerBuildAndPushResultLabel is { name: EdgegapWindowMetadata.CONTAINER_BUILD_AND_PUSH_RESULT_LABEL_ID },
  289. $"Expected {nameof(_containerBuildAndPushResultLabel)} via #{EdgegapWindowMetadata.CONTAINER_BUILD_AND_PUSH_RESULT_LABEL_ID}");
  290. Assert.IsTrue(_deploymentsFoldout is { name: EdgegapWindowMetadata.DEPLOYMENTS_FOLDOUT_ID },
  291. $"Expected {nameof(_deploymentsFoldout)} via #{EdgegapWindowMetadata.DEPLOYMENTS_FOLDOUT_ID}");
  292. Assert.IsTrue(_deploymentsRefreshBtn is { name: EdgegapWindowMetadata.DEPLOYMENTS_REFRESH_BTN_ID },
  293. $"Expected {nameof(_deploymentsRefreshBtn)} via #{EdgegapWindowMetadata.DEPLOYMENTS_REFRESH_BTN_ID}");
  294. Assert.IsTrue(_deploymentsCreateBtn is { name: EdgegapWindowMetadata.DEPLOYMENTS_CREATE_BTN_ID },
  295. $"Expected {nameof(_deploymentsCreateBtn)} via #{EdgegapWindowMetadata.DEPLOYMENTS_CREATE_BTN_ID}");
  296. Assert.IsTrue(_deploymentsStatusLabel is { name: EdgegapWindowMetadata.DEPLOYMENTS_STATUS_LABEL_ID },
  297. $"Expected {nameof(_deploymentsStatusLabel)} via #{EdgegapWindowMetadata.DEPLOYMENTS_STATUS_LABEL_ID}");
  298. Assert.IsTrue(_deploymentsServerDataContainer is { name: EdgegapWindowMetadata.DEPLOYMENTS_CONTAINER_ID },
  299. $"Expected {nameof(_deploymentsServerDataContainer)} via #{EdgegapWindowMetadata.DEPLOYMENTS_CONTAINER_ID}");
  300. Assert.IsTrue(_deploymentConnectionCopyUrlBtn is { name: EdgegapWindowMetadata.DEPLOYMENTS_CONNECTION_COPY_URL_BTN_ID },
  301. $"Expected {nameof(_deploymentConnectionCopyUrlBtn)} via #{EdgegapWindowMetadata.DEPLOYMENTS_CONNECTION_COPY_URL_BTN_ID}");
  302. Assert.IsTrue(_deploymentsConnectionUrlReadonlyInput is { name: EdgegapWindowMetadata.DEPLOYMENTS_CONNECTION_URL_READONLY_TXT_ID },
  303. $"Expected {nameof(_deploymentsConnectionUrlReadonlyInput)} via #{EdgegapWindowMetadata.DEPLOYMENTS_CONNECTION_URL_READONLY_TXT_ID}");
  304. Assert.IsTrue(_deploymentsConnectionStatusLabel is { name: EdgegapWindowMetadata.DEPLOYMENTS_CONNECTION_STATUS_LABEL_ID },
  305. $"Expected {nameof(_deploymentsConnectionStatusLabel)} via #{EdgegapWindowMetadata.DEPLOYMENTS_CONNECTION_STATUS_LABEL_ID}");
  306. Assert.IsTrue(_deploymentsConnectionStopBtn is { name: EdgegapWindowMetadata.DEPLOYMENTS_CONNECTION_SERVER_ACTION_STOP_BTN_ID },
  307. $"Expected {nameof(_deploymentsConnectionStopBtn)} via #{EdgegapWindowMetadata.DEPLOYMENTS_CONNECTION_SERVER_ACTION_STOP_BTN_ID}");
  308. Assert.IsTrue(_footerDocumentationBtn is { name: EdgegapWindowMetadata.FOOTER_DOCUMENTATION_BTN_ID },
  309. $"Expected {nameof(_footerDocumentationBtn)} via #{EdgegapWindowMetadata.FOOTER_DOCUMENTATION_BTN_ID}");
  310. Assert.IsTrue(_footerNeedMoreGameServersBtn is { name: EdgegapWindowMetadata.FOOTER_NEED_MORE_GAME_SERVERS_BTN_ID },
  311. $"Expected {nameof(_footerNeedMoreGameServersBtn)} via #{EdgegapWindowMetadata.FOOTER_NEED_MORE_GAME_SERVERS_BTN_ID}");
  312. // // TODO: Explicitly set, for now in v2 - but remember to assert later if we stop hard-coding these >>
  313. // _apiEnvironment
  314. // _appVersionName
  315. }
  316. catch (Exception e)
  317. {
  318. Debug.LogError(e.Message);
  319. _postAuthContainer.SetEnabled(false);
  320. }
  321. */ // END MIRROR CHANGE
  322. }
  323. /// <summary>
  324. /// Register non-btn change actionss. We'll want to save for persistence, validate, etc
  325. /// </summary>
  326. private void registerFieldCallbacks()
  327. {
  328. _apiTokenInput.RegisterValueChangedCallback(onApiTokenInputChanged);
  329. _apiTokenInput.RegisterCallback<FocusOutEvent>(onApiTokenInputFocusOut);
  330. _appNameInput.RegisterValueChangedCallback(onAppNameInputChanged);
  331. _containerPortNumInput.RegisterCallback<FocusOutEvent>(onContainerPortNumInputFocusOut);
  332. _containerUseCustomRegistryToggle.RegisterValueChangedCallback(onContainerUseCustomRegistryToggle);
  333. _containerNewTagVersionInput.RegisterValueChangedCallback(onContainerNewTagVersionInputChanged);
  334. }
  335. /// <summary>
  336. /// Prevents memory leaks, mysterious errors and "ghost" values set from a previous session.
  337. /// Should parity the opposute of registerFieldCallbacks().
  338. /// </summary>
  339. private void unregisterFieldCallbacks()
  340. {
  341. _apiTokenInput.UnregisterValueChangedCallback(onApiTokenInputChanged);
  342. _apiTokenInput.UnregisterCallback<FocusOutEvent>(onApiTokenInputFocusOut);
  343. _containerUseCustomRegistryToggle.UnregisterValueChangedCallback(onContainerUseCustomRegistryToggle);
  344. _containerPortNumInput.UnregisterCallback<FocusOutEvent>(onContainerPortNumInputFocusOut);
  345. }
  346. /// <summary>
  347. /// Register click actions, mostly from buttons: Need to -= unregistry them @ OnDisable()
  348. /// </summary>
  349. private void registerClickCallbacks()
  350. {
  351. _debugBtn.clickable.clicked += onDebugBtnClick;
  352. _apiTokenVerifyBtn.clickable.clicked += onApiTokenVerifyBtnClick;
  353. _apiTokenGetBtn.clickable.clicked += onApiTokenGetBtnClick;
  354. _appCreateBtn.clickable.clicked += onAppCreateBtnClickAsync;
  355. _appLoadExistingBtn.clickable.clicked += onAppLoadExistingBtnClickAsync;
  356. _containerBuildAndPushServerBtn.clickable.clicked += onContainerBuildAndPushServerBtnClickAsync;
  357. _deploymentConnectionCopyUrlBtn.clickable.clicked += onDeploymentConnectionCopyUrlBtnClick;
  358. _deploymentsRefreshBtn.clickable.clicked += onDeploymentsRefreshBtnClick;
  359. _deploymentsCreateBtn.clickable.clicked += onDeploymentCreateBtnClick;
  360. _footerDocumentationBtn.clickable.clicked += onFooterDocumentationBtnClick;
  361. _footerNeedMoreGameServersBtn.clickable.clicked += onFooterNeedMoreGameServersBtnClick;
  362. }
  363. /// <summary>
  364. /// Prevents memory leaks, mysterious errors and "ghost" values set from a previous session.
  365. /// Should parity the opposute of registerClickEvents().
  366. /// </summary>
  367. private void unregisterClickEvents()
  368. {
  369. _debugBtn.clickable.clicked -= onDebugBtnClick;
  370. _apiTokenVerifyBtn.clickable.clicked -= onApiTokenVerifyBtnClick;
  371. _apiTokenGetBtn.clickable.clicked -= onApiTokenGetBtnClick;
  372. _appCreateBtn.clickable.clicked -= onAppCreateBtnClickAsync;
  373. _appLoadExistingBtn.clickable.clicked -= onAppLoadExistingBtnClickAsync;
  374. _containerBuildAndPushServerBtn.clickable.clicked -= onContainerBuildAndPushServerBtnClickAsync;
  375. _deploymentConnectionCopyUrlBtn.clickable.clicked -= onDeploymentConnectionCopyUrlBtnClick;
  376. _deploymentsRefreshBtn.clickable.clicked -= onDeploymentsRefreshBtnClick;
  377. _deploymentsCreateBtn.clickable.clicked -= onDeploymentCreateBtnClick;
  378. _footerDocumentationBtn.clickable.clicked -= onFooterDocumentationBtnClick;
  379. _footerNeedMoreGameServersBtn.clickable.clicked -= onFooterNeedMoreGameServersBtnClick;
  380. }
  381. private void initToggleDynamicUi()
  382. {
  383. hideResultLabels();
  384. _deploymentsRefreshBtn.SetEnabled(false);
  385. loadPersistentDataFromEditorPrefs();
  386. setDeploymentBtnsFromCache();
  387. _debugBtn.visible = EdgegapWindowMetadata.SHOW_DEBUG_BTN;
  388. }
  389. /// <summary>
  390. /// Based on existing _deploymentsConnection[Url|Status]Label txt
  391. /// </summary>
  392. private void setDeploymentBtnsFromCache()
  393. {
  394. bool showDeploymentConnectionStopBtn = !string.IsNullOrEmpty(_deploymentsConnectionUrlReadonlyInput.text);
  395. if (!showDeploymentConnectionStopBtn)
  396. return;
  397. // We found some leftover connection cache >>
  398. _deploymentsConnectionStopBtn.visible = true;
  399. _deploymentsRefreshBtn.SetEnabled(true);
  400. // Enable stop btn?
  401. bool isDeployed = _deploymentsConnectionStatusLabel.text.ToLowerInvariant().Contains("deployed");
  402. _deploymentsConnectionStopBtn.SetEnabled(isDeployed);
  403. if (isDeployed)
  404. _deploymentsConnectionStopBtn.clickable.clickedWithEventInfo += onDynamicStopServerBtnAsync; // Unsub'd from within
  405. }
  406. /// <summary>For example, result labels (success/err) should be hidden on init</summary>
  407. private void hideResultLabels()
  408. {
  409. _appCreateResultLabel.visible = false;
  410. _containerBuildAndPushResultLabel.visible = false;
  411. _deploymentsStatusLabel.style.display = DisplayStyle.None;
  412. }
  413. #region Init -> Button clicks
  414. /// <summary>
  415. /// Experiment here! You may want to log what you're doing
  416. /// in case you inadvertently leave it on.
  417. /// </summary>
  418. private void onDebugBtnClick() => debugEnableAllGroups();
  419. private void debugEnableAllGroups()
  420. {
  421. Debug.Log("debugEnableAllGroups");
  422. _appInfoFoldout.SetEnabled(true);
  423. _appInfoFoldout.SetEnabled(true);
  424. _containerRegistryFoldout.SetEnabled(true);
  425. _deploymentsFoldout.SetEnabled(true);
  426. if (_containerUseCustomRegistryToggle.value)
  427. _containerCustomRegistryWrapper.SetEnabled(true);
  428. }
  429. private void onApiTokenVerifyBtnClick() => _ = verifyApiTokenGetRegistryCredsAsync();
  430. private void onApiTokenGetBtnClick() => openGetApiTokenWebsite();
  431. /// <summary>Process UI + validation before/after API logic</summary>
  432. private async void onAppCreateBtnClickAsync()
  433. {
  434. // Assert data locally before calling API
  435. assertAppNameExists();
  436. _appCreateResultLabel.text = EdgegapWindowMetadata.WrapRichTextInColor("Creating...",
  437. EdgegapWindowMetadata.StatusColors.Processing);
  438. try { await createAppAsync(); }
  439. finally
  440. {
  441. _appCreateBtn.SetEnabled(checkHasAppName());
  442. _appCreateResultLabel.visible = _appCreateResultLabel.text != EdgegapWindowMetadata.LOADING_RICH_STR;
  443. }
  444. }
  445. /// <summary>Process UI + validation before/after API logic</summary>
  446. private async void onAppLoadExistingBtnClickAsync()
  447. {
  448. // Assert UI data locally before calling API
  449. assertAppNameExists();
  450. try { await GetAppAsync(); }
  451. finally
  452. {
  453. _appLoadExistingBtn.SetEnabled(checkHasAppName());
  454. _appCreateResultLabel.visible = _appCreateResultLabel.text != EdgegapWindowMetadata.LOADING_RICH_STR;
  455. }
  456. }
  457. /// <summary>Copy url to clipboard</summary>
  458. private void onDeploymentConnectionCopyUrlBtnClick()
  459. {
  460. if (string.IsNullOrEmpty(_deploymentsConnectionUrlReadonlyInput.value))
  461. return; // Nothing to copy
  462. EditorGUIUtility.systemCopyBuffer = _deploymentsConnectionUrlReadonlyInput.value;
  463. _deploymentsStatusLabel.text = EdgegapWindowMetadata.WrapRichTextInColor("Copied URL!",
  464. EdgegapWindowMetadata.StatusColors.Success);
  465. _deploymentsStatusLabel.style.display = DisplayStyle.Flex;
  466. _ = clearDeploymentStatusAfterDelay(seconds: 1);
  467. }
  468. private async Task clearDeploymentStatusAfterDelay(int seconds)
  469. {
  470. await Task.Delay(TimeSpan.FromSeconds(seconds));
  471. _deploymentsStatusLabel.style.display = DisplayStyle.None;
  472. }
  473. /// <summary>Process UI + validation before/after API logic</summary>
  474. private async void onContainerBuildAndPushServerBtnClickAsync()
  475. {
  476. // Assert data locally before calling API
  477. // Validate custom container registry, app name
  478. try
  479. {
  480. assertAppNameExists();
  481. Assert.IsTrue(
  482. !_containerImageRepositoryInput.value.EndsWith("/"),
  483. $"Expected {nameof(_containerImageRepositoryInput)} to !contain " +
  484. "trailing slash (should end with /appName)");
  485. }
  486. catch (Exception e)
  487. {
  488. Debug.LogError($"onContainerBuildAndPushServerBtnClickAsync Error: {e}");
  489. throw;
  490. }
  491. // Hide previous result labels, disable btns (to reenable when done)
  492. hideResultLabels();
  493. _containerBuildAndPushServerBtn.SetEnabled(false);
  494. // Show new loading status
  495. _containerBuildAndPushResultLabel.text = EdgegapWindowMetadata.WrapRichTextInColor(
  496. EdgegapWindowMetadata.PROCESSING_RICH_STR,
  497. EdgegapWindowMetadata.StatusColors.Processing);
  498. try { await buildAndPushServerAsync(); }
  499. finally
  500. {
  501. _containerBuildAndPushServerBtn.SetEnabled(checkHasAppName());
  502. _containerBuildAndPushResultLabel.visible = _containerBuildAndPushResultLabel.text != EdgegapWindowMetadata.PROCESSING_RICH_STR;
  503. }
  504. }
  505. private bool checkHasAppName() => _appNameInput.value.Length > 0;
  506. private void onDeploymentsRefreshBtnClick() => _ = refreshDeploymentsAsync();
  507. private void onFooterDocumentationBtnClick() => openDocumentationWebsite();
  508. private void onFooterNeedMoreGameServersBtnClick() => openNeedMoreGameServersWebsite();
  509. /// <summary>AKA "Create New Deployment" Btn</summary>
  510. private void onDeploymentCreateBtnClick() => _ = createDeploymentStartServerAsync();
  511. #endregion // Init -> /Button Clicks
  512. #endregion // Init
  513. /// <summary>Throw if !appName val</summary>
  514. private void assertAppNameExists() =>
  515. Assert.IsTrue(!string.IsNullOrEmpty(_appNameInput.value),
  516. $"Expected {nameof(_appNameInput)} val");
  517. /// <summary>
  518. /// Save persistent read-only data: If the human didn't type it, it won't save automatically.
  519. /// </summary>
  520. private void SyncObjectWithForm()
  521. {
  522. _appIconSpriteObj = _appIconSpriteObjInput.value as Sprite;
  523. }
  524. /// <summary>TODO: Load persistent data?</summary>
  525. private void syncFormWithObjectStatic()
  526. {
  527. // Only show the rest of the form if apiToken is verified
  528. _postAuthContainer.SetEnabled(_isApiTokenVerified);
  529. _appIconSpriteObjInput.value = _appIconSpriteObj;
  530. _containerCustomRegistryWrapper.SetEnabled(_containerUseCustomRegistryToggle.value);
  531. _containerUseCustomRegistryToggle.value = _containerUseCustomRegistryToggle.value;
  532. // Only enable certain elements if appName exists
  533. bool hasAppName = checkHasAppName();
  534. _appCreateBtn.SetEnabled(hasAppName);
  535. _appLoadExistingBtn.SetEnabled(hasAppName);
  536. }
  537. /// <summary>
  538. /// Dynamically set form based on API call results.
  539. /// => If APIToken is cached via EditorPrefs, verify => gets registry creds.
  540. /// => If appName is cached via ViewDataKey, loads the app.
  541. /// </summary>
  542. private async Task syncFormWithObjectDynamicAsync()
  543. {
  544. if (string.IsNullOrEmpty(_apiTokenInput.value))
  545. return;
  546. // We found a cached api token: Verify =>
  547. if (IsLogLevelDebug) Debug.Log("syncFormWithObjectDynamicAsync: Found apiToken; " +
  548. "calling verifyApiTokenGetRegistryCredsAsync =>");
  549. await verifyApiTokenGetRegistryCredsAsync();
  550. // Was the API token verified + we found a cached app name? Load the app =>
  551. // But ignore errs, since we're just *assuming* the app exists since the appName was filled
  552. if (_isApiTokenVerified && checkHasAppName())
  553. {
  554. if (IsLogLevelDebug) Debug.Log("syncFormWithObjectDynamicAsync: Found apiToken && appName; " +
  555. "calling GetAppAsync =>");
  556. try { await GetAppAsync(); }
  557. finally { _appLoadExistingBtn.SetEnabled(checkHasAppName()); }
  558. }
  559. }
  560. #region Immediate non-button changes
  561. /// <summary>
  562. /// On change, validate -> update custom container registry suffix.
  563. /// Toggle create app btn if 1+ char
  564. /// </summary>
  565. /// <param name="evt"></param>
  566. private void onAppNameInputChanged(ChangeEvent<string> evt)
  567. {
  568. // Validate: Only allow alphanumeric, underscore, dash, plus, period
  569. if (!_appNameAllowedCharsRegex.IsMatch(evt.newValue))
  570. _appNameInput.value = evt.previousValue; // Revert to the previous value
  571. else
  572. setContainerImageRepositoryVal(); // Valid -> Update the custom container registry suffix
  573. // Toggle btns on 1+ char entered
  574. bool hasAppName = checkHasAppName();
  575. _appCreateBtn.SetEnabled(hasAppName);
  576. _appLoadExistingBtn.SetEnabled(hasAppName);
  577. }
  578. /// <summary>On focus out, clamp port between 1024~49151</summary>
  579. /// <param name="evt"></param>
  580. private void onContainerPortNumInputFocusOut(FocusOutEvent evt)
  581. {
  582. // Use TryParse to avoid exceptions
  583. if (int.TryParse(_containerPortNumInput.value, out int port))
  584. {
  585. // Clamp the port to the range and set the value back to the TextField
  586. _containerPortNumInput.value = Mathf.Clamp(
  587. port,
  588. EdgegapWindowMetadata.PORT_MIN,
  589. EdgegapWindowMetadata.PORT_MAX)
  590. .ToString();
  591. }
  592. else
  593. {
  594. // If input is !valid, set to default
  595. _containerPortNumInput.value = EdgegapWindowMetadata.PORT_DEFAULT.ToString();
  596. }
  597. }
  598. /// <summary>
  599. /// While changing the token, we temporarily unmask. On change, set state to !verified.
  600. /// </summary>
  601. /// <param name="evt"></param>
  602. private void onApiTokenInputChanged(ChangeEvent<string> evt)
  603. {
  604. // Unmask while changing
  605. TextField apiTokenTxt = evt.target as TextField;
  606. apiTokenTxt.isPasswordField = false;
  607. // Token changed? Reset form to !verified state and fold all groups
  608. _isApiTokenVerified = false;
  609. _postAuthContainer.SetEnabled(false);
  610. closeDisableGroups();
  611. // Toggle "Verify" btn on 1+ char entered
  612. _apiTokenVerifyBtn.SetEnabled(evt.newValue.Length > 0);
  613. }
  614. /// <summary>Unmask while typing</summary>
  615. /// <param name="evt"></param>
  616. private void onApiTokenInputFocusOut(FocusOutEvent evt)
  617. {
  618. TextField apiTokenTxt = evt.target as TextField;
  619. apiTokenTxt.isPasswordField = true;
  620. }
  621. /// <summary>On toggle, enable || disable the custom registry inputs (below the Toggle).</summary>
  622. private void onContainerUseCustomRegistryToggle(ChangeEvent<bool> evt) =>
  623. _containerCustomRegistryWrapper.SetEnabled(evt.newValue);
  624. /// <summary>On empty, we fallback to "latest", a fallback val from EdgegapWindowMetadata.cs</summary>
  625. /// <param name="evt"></param>
  626. private void onContainerNewTagVersionInputChanged(ChangeEvent<string> evt)
  627. {
  628. if (!string.IsNullOrEmpty(evt.newValue))
  629. return;
  630. // Set fallback value -> select all for UX, since the user may not expect this
  631. _containerNewTagVersionInput.value = EdgegapWindowMetadata.DEFAULT_VERSION_TAG;
  632. _containerNewTagVersionInput.SelectAll();
  633. }
  634. #endregion // Immediate non-button changes
  635. /// <summary>
  636. /// Used for converting a Sprite to a base64 string: By default, textures are !readable,
  637. /// and we don't want to have to instruct users how to make it readable for UX.
  638. /// Instead, we'll make a copy of that texture -> make it readable.
  639. /// </summary>
  640. /// <param name="original"></param>
  641. /// <returns></returns>
  642. private Texture2D makeTextureReadable(Texture2D original)
  643. {
  644. RenderTexture rt = RenderTexture.GetTemporary(
  645. original.width,
  646. original.height
  647. );
  648. Graphics.Blit(original, rt);
  649. Texture2D readableTexture = new Texture2D(original.width, original.height);
  650. Rect rect = new Rect(
  651. 0,
  652. 0,
  653. rt.width,
  654. rt.height);
  655. readableTexture.ReadPixels(rect, destX: 0, destY: 0);
  656. readableTexture.Apply();
  657. RenderTexture.ReleaseTemporary(rt);
  658. return readableTexture;
  659. }
  660. /// <summary>From Base64 string -> to Sprite</summary>
  661. /// <param name="imgBase64Str">Edgegap build app requires a max size of 200</param>
  662. /// <returns>Sprite</returns>
  663. private Sprite getSpriteFromBase64Str(string imgBase64Str)
  664. {
  665. if (string.IsNullOrEmpty(imgBase64Str))
  666. return null;
  667. try
  668. {
  669. byte[] imageBytes = Convert.FromBase64String(imgBase64Str);
  670. Texture2D texture = new Texture2D(2, 2);
  671. texture.LoadImage(imageBytes);
  672. Rect rect = new Rect( // MIRROR CHANGE: 'new()' not supported in Unity 2020
  673. x: 0.0f,
  674. y: 0.0f,
  675. texture.width,
  676. texture.height);
  677. return Sprite.Create(
  678. texture,
  679. rect,
  680. pivot: new Vector2(0.5f, 0.5f),
  681. pixelsPerUnit: 100.0f);
  682. }
  683. catch (Exception e)
  684. {
  685. Debug.Log($"Warning: getSpriteFromBase64Str failed (returning null) - {e}");
  686. return null;
  687. }
  688. }
  689. /// <summary>From Sprite -> to Base64 string</summary>
  690. /// <param name="sprite"></param>
  691. /// <param name="maxKbSize">Edgegap build app requires a max size of 200</param>
  692. /// <returns>imageBase64Str</returns>
  693. private string getBase64StrFromSprite(Sprite sprite, int maxKbSize = 200)
  694. {
  695. if (sprite == null)
  696. return null;
  697. try
  698. {
  699. Texture2D texture = makeTextureReadable(sprite.texture);
  700. // Crop the texture to the sprite's rectangle (instead of the entire texture)
  701. Texture2D croppedTexture = new Texture2D(
  702. (int)sprite.rect.width,
  703. (int)sprite.rect.height);
  704. Color[] pixels = texture.GetPixels(
  705. (int)sprite.rect.x,
  706. (int)sprite.rect.y,
  707. (int)sprite.rect.width,
  708. (int)sprite.rect.height
  709. );
  710. croppedTexture.SetPixels(pixels);
  711. croppedTexture.Apply();
  712. // Encode to PNG ->
  713. byte[] textureBytes = croppedTexture.EncodeToPNG();
  714. // Validate size
  715. const int oneKb = 1024;
  716. int pngTextureSizeKb = textureBytes.Length / oneKb;
  717. bool isPngLessThanMaxSize = pngTextureSizeKb < maxKbSize;
  718. if (!isPngLessThanMaxSize)
  719. {
  720. textureBytes = croppedTexture.EncodeToJPG();
  721. int jpgTextureSizeKb = textureBytes.Length / oneKb;
  722. bool isJpgLessThanMaxSize = pngTextureSizeKb < maxKbSize;
  723. Assert.IsTrue(isJpgLessThanMaxSize, $"Expected texture PNG to be < {maxKbSize}kb " +
  724. $"in size (but found {jpgTextureSizeKb}kb); then tried JPG, but is still {jpgTextureSizeKb}kb in size");
  725. Debug.LogWarning($"App icon PNG was too large (max {maxKbSize}), so we converted to JPG");
  726. }
  727. string base64ImageString = Convert.ToBase64String(textureBytes); // eg: "Aaabbcc=="
  728. return base64ImageString;
  729. }
  730. catch (Exception e)
  731. {
  732. Debug.LogError($"Error: {e.Message}");
  733. return null;
  734. }
  735. }
  736. /// <summary>
  737. /// Verifies token => apps/container groups -> gets registry creds (if any).
  738. /// TODO: UX - Show loading spinner.
  739. /// </summary>
  740. private async Task verifyApiTokenGetRegistryCredsAsync()
  741. {
  742. if (IsLogLevelDebug) Debug.Log("verifyApiTokenGetRegistryCredsAsync");
  743. // Disable most ui while we verify
  744. _isApiTokenVerified = false;
  745. _apiTokenVerifyBtn.SetEnabled(false);
  746. SyncContainerEnablesToState();
  747. hideResultLabels();
  748. EdgegapWizardApi wizardApi = getWizardApi();
  749. EdgegapHttpResult initQuickStartResultCode = await wizardApi.InitQuickStart();
  750. _apiTokenVerifyBtn.SetEnabled(true);
  751. _isApiTokenVerified = initQuickStartResultCode.IsResultCode204;
  752. if (!_isApiTokenVerified)
  753. {
  754. SyncContainerEnablesToState();
  755. return;
  756. }
  757. // Verified: Let's see if we have active registry credentials // TODO: This will later be a result model
  758. EdgegapHttpResult<GetRegistryCredentialsResult> getRegistryCredentialsResult = await wizardApi.GetRegistryCredentials();
  759. if (getRegistryCredentialsResult.IsResultCode200)
  760. {
  761. // Success
  762. _credentials = getRegistryCredentialsResult.Data;
  763. persistUnmaskedApiToken(_apiTokenInput.value);
  764. prefillContainerRegistryForm(_credentials);
  765. }
  766. else
  767. {
  768. // Fail
  769. }
  770. // Unlock the rest of the form, whether we prefill the container registry or not
  771. SyncContainerEnablesToState();
  772. }
  773. /// <summary>
  774. /// We have container registry params; we'll prefill registry container fields.
  775. /// </summary>
  776. /// <param name="credentials">GetRegistryCredentialsResult</param>
  777. private void prefillContainerRegistryForm(GetRegistryCredentialsResult credentials)
  778. {
  779. if (IsLogLevelDebug) Debug.Log("prefillContainerRegistryForm");
  780. if (credentials == null)
  781. throw new Exception($"!{nameof(credentials)}");
  782. _containerRegistryUrlInput.value = credentials.RegistryUrl;
  783. setContainerImageRepositoryVal();
  784. _containerUsernameInput.value = credentials.Username;
  785. _containerTokenInput.value = credentials.Token;
  786. }
  787. /// <summary>
  788. /// Sets to "{credentials.Project}/{appName}" from cached credentials, forcing lowercased appName.
  789. /// </summary>
  790. private void setContainerImageRepositoryVal()
  791. {
  792. // ex: "xblade1-9sa8dfh9sda8hf/mygame1"
  793. string project = _credentials?.Project ?? "";
  794. string appName = _appNameInput?.value.ToLowerInvariant() ?? "";
  795. _containerImageRepositoryInput.value = $"{project}/{appName}";
  796. }
  797. public static string Base64Encode(string plainText)
  798. {
  799. byte[] plainBytes = Encoding.UTF8.GetBytes(plainText);
  800. return Convert.ToBase64String(plainBytes);
  801. }
  802. public static string Base64Decode(string base64EncodedText)
  803. {
  804. byte[] base64Bytes = Convert.FromBase64String(base64EncodedText);
  805. return Encoding.UTF8.GetString(base64Bytes);
  806. }
  807. /// <summary>
  808. /// Toggle container groups and foldouts on/off based on:
  809. /// - _isApiTokenVerified
  810. /// </summary>
  811. private void SyncContainerEnablesToState()
  812. {
  813. // Requires _isApiTokenVerified
  814. _postAuthContainer.SetEnabled(_isApiTokenVerified); // Entire body container
  815. _appInfoFoldout.SetEnabled(_isApiTokenVerified);
  816. _appInfoFoldout.value = _isApiTokenVerified;
  817. // + Requires _isContainerRegistryReady
  818. bool isApiTokenVerifiedAndContainerReady = _isApiTokenVerified && _isContainerRegistryReady;
  819. _containerRegistryFoldout.SetEnabled(isApiTokenVerifiedAndContainerReady);
  820. _containerRegistryFoldout.value = isApiTokenVerifiedAndContainerReady;
  821. _deploymentsFoldout.SetEnabled(isApiTokenVerifiedAndContainerReady);
  822. _deploymentsFoldout.value = isApiTokenVerifiedAndContainerReady;
  823. // + Requires _containerUseCustomRegistryToggleBool
  824. _containerCustomRegistryWrapper.SetEnabled(isApiTokenVerifiedAndContainerReady &&
  825. _containerUseCustomRegistryToggle.value);
  826. }
  827. private void openGetApiTokenWebsite()
  828. {
  829. if (IsLogLevelDebug) Debug.Log("openGetApiTokenWebsite");
  830. Application.OpenURL(EdgegapWindowMetadata.EDGEGAP_GET_A_TOKEN_URL);
  831. }
  832. /// <returns>isSuccess; sets _isContainerRegistryReady + _loadedApp</returns>
  833. private async Task<bool> GetAppAsync()
  834. {
  835. if (IsLogLevelDebug) Debug.Log("GetAppAsync");
  836. // Hide previous result labels, disable btns (to reenable when done)
  837. hideResultLabels();
  838. _appCreateBtn.SetEnabled(false);
  839. _apiTokenVerifyBtn.SetEnabled(false);
  840. // Show new loading status
  841. _appCreateResultLabel.text = EdgegapWindowMetadata.WrapRichTextInColor(
  842. EdgegapWindowMetadata.LOADING_RICH_STR,
  843. EdgegapWindowMetadata.StatusColors.Processing);
  844. _appCreateResultLabel.visible = true;
  845. EdgegapAppApi appApi = getAppApi();
  846. EdgegapHttpResult<GetCreateAppResult> getAppResult = await appApi.GetApp(_appNameInput.value);
  847. onGetCreateApplicationResult(getAppResult);
  848. return _isContainerRegistryReady;
  849. }
  850. /// <summary>
  851. /// TODO: Add err handling for reaching app limit (max 2 for free tier).
  852. /// </summary>
  853. private async Task createAppAsync()
  854. {
  855. if (IsLogLevelDebug) Debug.Log("createAppAsync");
  856. // Hide previous result labels, disable btns (to reenable when done)
  857. hideResultLabels();
  858. _appCreateBtn.SetEnabled(false);
  859. _apiTokenVerifyBtn.SetEnabled(false);
  860. EdgegapAppApi appApi = getAppApi();
  861. CreateAppRequest createAppRequest = new CreateAppRequest( // MIRROR CHANGE: 'new()' not supported in Unity 2020
  862. _appNameInput.value,
  863. isActive: true,
  864. getBase64StrFromSprite(_appIconSpriteObj) ?? "");
  865. EdgegapHttpResult<GetCreateAppResult> createAppResult = await appApi.CreateApp(createAppRequest);
  866. onGetCreateApplicationResult(createAppResult);
  867. }
  868. /// <summary>Get || Create results both handled here. On success, sets _isContainerRegistryReady + _loadedApp data</summary>
  869. /// <param name="result"></param>
  870. private void onGetCreateApplicationResult(EdgegapHttpResult<GetCreateAppResult> result)
  871. {
  872. // Assert the result itself || result's create time exists
  873. bool isSuccess = result.IsResultCode200 || result.IsResultCode409; // 409 == app already exists
  874. _isContainerRegistryReady = isSuccess;
  875. _loadedApp = result.Data;
  876. _appCreateResultLabel.text = getFriendlyCreateAppResultStr(result);
  877. _containerRegistryFoldout.value = _isContainerRegistryReady;
  878. _appCreateBtn.SetEnabled(true);
  879. _apiTokenVerifyBtn.SetEnabled(true);
  880. SyncContainerEnablesToState();
  881. // Only show status label if we're init'd; otherwise, we auto-tried to get the existing app that
  882. // we knew had a chance of not being there
  883. _appCreateResultLabel.visible = IsInitd;
  884. // App base64 img? Parse to sprite, overwrite app image UI/cache
  885. if (!string.IsNullOrEmpty(_loadedApp.Image))
  886. {
  887. _appIconSpriteObj = getSpriteFromBase64Str(_loadedApp.Image);
  888. _appIconSpriteObjInput.value = _appIconSpriteObj;
  889. }
  890. // On fail, shake the "Add more game servers" btn // 400 == # of apps limit reached
  891. bool isCreate = result.HttpMethod == HttpMethod.Post;
  892. bool isCreateFailAppNumCapMaxed = isCreate && !_isContainerRegistryReady && result.IsResultCode400;
  893. if (isCreateFailAppNumCapMaxed)
  894. shakeNeedMoreGameServersBtn();
  895. }
  896. /// <summary>Slight animation shake</summary>
  897. private void shakeNeedMoreGameServersBtn()
  898. {
  899. ButtonShaker shaker = new ButtonShaker(_footerNeedMoreGameServersBtn);
  900. _ = shaker.ApplyShakeAsync();
  901. }
  902. /// <returns>Generally "Success" || "Error: {error}" || "Warning: {error}"</returns>
  903. private string getFriendlyCreateAppResultStr(EdgegapHttpResult<GetCreateAppResult> createAppResult)
  904. {
  905. string coloredResultStr = null;
  906. if (!_isContainerRegistryReady)
  907. {
  908. // Error
  909. string resultStr = $"<b>Error:</b> {createAppResult?.Error?.ErrorMessage}";
  910. coloredResultStr = EdgegapWindowMetadata.WrapRichTextInColor(
  911. resultStr, EdgegapWindowMetadata.StatusColors.Error);
  912. }
  913. else if (createAppResult.IsResultCode409)
  914. {
  915. // Warn: App already exists - Still success, but just a warn
  916. string resultStr = $"<b>Warning:</b> {createAppResult.Error.ErrorMessage}";
  917. coloredResultStr = EdgegapWindowMetadata.WrapRichTextInColor(
  918. resultStr, EdgegapWindowMetadata.StatusColors.Warn);
  919. }
  920. else
  921. {
  922. // Success
  923. coloredResultStr = EdgegapWindowMetadata.WrapRichTextInColor(
  924. "Success", EdgegapWindowMetadata.StatusColors.Success);
  925. }
  926. return coloredResultStr;
  927. }
  928. /// <summary>Open contact form in desired locale</summary>
  929. private void openNeedMoreGameServersWebsite() =>
  930. Application.OpenURL(EdgegapWindowMetadata.EDGEGAP_ADD_MORE_GAME_SERVERS_URL);
  931. private void openDocumentationWebsite()
  932. {
  933. // MIRROR CHANGE
  934. /*
  935. string documentationUrl = _apiEnvironment.GetDocumentationUrl();
  936. if (!string.IsNullOrEmpty(documentationUrl))
  937. Application.OpenURL(documentationUrl);
  938. else
  939. {
  940. string apiEnvName = Enum.GetName(typeof(ApiEnvironment), _apiEnvironment);
  941. Debug.LogWarning($"Could not open documentation for api environment " +
  942. $"{apiEnvName}: No documentation URL.");
  943. }
  944. */
  945. // link to our step by step guide
  946. Application.OpenURL("https://mirror-networking.gitbook.io/docs/hosting/edgegap-hosting-plugin-guide");
  947. // END MIRROR CHANGE
  948. }
  949. /// <summary>
  950. /// Currently only refreshes an existing deployment. AKA "OnRefresh".
  951. /// TODO: Consider dynamically adding the entire list via GET all deployments.
  952. /// </summary>
  953. private async Task refreshDeploymentsAsync()
  954. {
  955. if (IsLogLevelDebug) Debug.Log("refreshDeploymentsAsync");
  956. // Sanity check requestId - if refreshBtn is enabled, we *should* have it
  957. if (string.IsNullOrEmpty(_deploymentRequestId))
  958. {
  959. // We must have stale data - reset
  960. clearDeploymentConnections();
  961. return;
  962. }
  963. hideResultLabels();
  964. // clearDeploymentConnections(); // We want to leave the old URL while we only have one
  965. _deploymentsConnectionStatusLabel.text = EdgegapWindowMetadata.WrapRichTextInColor(
  966. "<i>Refreshing...</i>", EdgegapWindowMetadata.StatusColors.Processing);
  967. EdgegapDeploymentsApi deployApi = getDeployApi();
  968. EdgegapHttpResult<GetDeploymentStatusResult> getDeploymentStatusResponse =
  969. await deployApi.GetDeploymentStatusAsync(_deploymentRequestId);
  970. bool isActiveStatus = getDeploymentStatusResponse?.StatusCode != null &&
  971. getDeploymentStatusResponse.Data.CurrentStatus == EdgegapWindowMetadata.READY_STATUS;
  972. if (isActiveStatus)
  973. onCreateDeploymentOrRefreshSuccess(getDeploymentStatusResponse.Data);
  974. else
  975. {
  976. onCreateDeploymentStartServerFail();
  977. if (!getDeploymentStatusResponse.HasErr)
  978. onRefreshDeploymentStoppedStatus(); // Only a "soft" fail - not a true err
  979. }
  980. }
  981. /// <summary>The deployment simply stopped (a "soft" fail - not an actual err)</summary>
  982. private void onRefreshDeploymentStoppedStatus()
  983. {
  984. // Override to Create fail -- instead, we've simplfy stopped (a "soft" fail)
  985. _deploymentsConnectionStatusLabel.text = getConnectionStoppedRichStr();
  986. _deploymentsStatusLabel.style.display = DisplayStyle.None;
  987. _deploymentsConnectionStopBtn.SetEnabled(false);
  988. _deploymentsConnectionStopBtn.visible = true;
  989. }
  990. /// <summary>Don't use this if you want to keep the last-known connection info.</summary>
  991. private void clearDeploymentConnections()
  992. {
  993. _deploymentsConnectionUrlReadonlyInput.value = "";
  994. _deploymentsConnectionStatusLabel.text = "";
  995. _deploymentsConnectionStopBtn.visible = false;
  996. _deploymentsRefreshBtn.SetEnabled(false);
  997. }
  998. /// <summary>
  999. /// V2 Successor to legacy startServerCallbackAsync() from "Create New Deployment" Btn.
  1000. /// </summary>
  1001. private async Task createDeploymentStartServerAsync()
  1002. {
  1003. // Hide previous result labels, disable btns (to reenable when done)
  1004. if (IsLogLevelDebug) Debug.Log("createDeploymentStartServerAsync");
  1005. hideResultLabels();
  1006. _deploymentsCreateBtn.SetEnabled(false);
  1007. _deploymentsRefreshBtn.SetEnabled(false);
  1008. // _deploymentsConnectionUrlReadonlyInput.value = ""; // We currently want to keep the last known connection, even on err
  1009. _deploymentsConnectionStatusLabel.text = EdgegapWindowMetadata.WrapRichTextInColor(
  1010. EdgegapWindowMetadata.DEPLOY_REQUEST_RICH_STR,
  1011. EdgegapWindowMetadata.StatusColors.Processing);
  1012. try
  1013. {
  1014. EdgegapDeploymentsApi deployApi = getDeployApi();
  1015. // Get (+cache) external IP async, required to create a deployment. Prioritize cache.
  1016. _userExternalIp = await getExternalIpAddress();
  1017. CreateDeploymentRequest createDeploymentReq = new CreateDeploymentRequest( // MIRROR CHANGE: 'new()' not supported in Unity 2020
  1018. _appNameInput.value,
  1019. _containerNewTagVersionInput.value,
  1020. _userExternalIp);
  1021. // Request to deploy (it won't be active, yet) =>
  1022. EdgegapHttpResult<CreateDeploymentResult> createDeploymentResponse =
  1023. await deployApi.CreateDeploymentAsync(createDeploymentReq);
  1024. if (!createDeploymentResponse.IsResultCode200)
  1025. {
  1026. onCreateDeploymentStartServerFail(createDeploymentResponse);
  1027. return;
  1028. }
  1029. else
  1030. {
  1031. // Update status
  1032. _deploymentsConnectionStatusLabel.text = EdgegapWindowMetadata.WrapRichTextInColor(
  1033. "<i>Deploying...</i>", EdgegapWindowMetadata.StatusColors.Processing);
  1034. }
  1035. // Check the status of the deployment for READY every 2s =>
  1036. const int pollIntervalSecs = EdgegapWindowMetadata.DEPLOYMENT_READY_STATUS_POLL_SECONDS;
  1037. EdgegapHttpResult<GetDeploymentStatusResult> getDeploymentStatusResponse = await deployApi.AwaitReadyStatusAsync(
  1038. createDeploymentResponse.Data.RequestId,
  1039. TimeSpan.FromSeconds(pollIntervalSecs));
  1040. // Process create deployment response
  1041. bool isSuccess = createDeploymentResponse.IsResultCode200;
  1042. if (isSuccess)
  1043. onCreateDeploymentOrRefreshSuccess(getDeploymentStatusResponse.Data);
  1044. else
  1045. onCreateDeploymentStartServerFail(createDeploymentResponse);
  1046. _deploymentsStatusLabel.style.display = DisplayStyle.Flex;
  1047. }
  1048. finally
  1049. {
  1050. _deploymentsCreateBtn.SetEnabled(true);
  1051. }
  1052. }
  1053. /// <summary>
  1054. /// CreateDeployment || RefreshDeployment success handler.
  1055. /// </summary>
  1056. /// <param name="getDeploymentStatusResult">Only pass from CreateDeployment</param>
  1057. private void onCreateDeploymentOrRefreshSuccess(GetDeploymentStatusResult getDeploymentStatusResult)
  1058. {
  1059. // Success
  1060. hideResultLabels();
  1061. _deploymentsStatusLabel.text = EdgegapWindowMetadata.WrapRichTextInColor(
  1062. "Success", EdgegapWindowMetadata.StatusColors.Success);
  1063. _deploymentsStatusLabel.style.display = DisplayStyle.Flex;
  1064. // Cache the deployment result -> persist the requestId
  1065. _lastKnownDeployment = getDeploymentStatusResult;
  1066. _deploymentRequestId = getDeploymentStatusResult.RequestId;
  1067. EditorPrefs.SetString(EdgegapWindowMetadata.DEPLOYMENT_REQUEST_ID_KEY_STR, _deploymentRequestId);
  1068. // ------------
  1069. // Set the static connection row label data >>
  1070. // TODO: This will be dynamically inserted via MVC-style template when we support multiple deployments >>
  1071. // Get external port
  1072. // BUG(WORKAROUND): Expected `ports` to be List<AppPortsData>, but received Dictionary<string, AppPortsData>
  1073. KeyValuePair<string, DeploymentPortsData> portsDataKvp = getDeploymentStatusResult.PortsDict.FirstOrDefault();
  1074. Assert.IsNotNull(portsDataKvp.Value, $"Expected ({nameof(portsDataKvp)} from `getDeploymentStatusResult.PortsDict`)");
  1075. DeploymentPortsData deploymentPortData = portsDataKvp.Value;
  1076. string externalPortStr = deploymentPortData.External.ToString();
  1077. string domainWithExternalPort = $"{getDeploymentStatusResult.Fqdn}:{externalPortStr}";
  1078. _deploymentsConnectionUrlReadonlyInput.value = domainWithExternalPort;
  1079. string newConnectionStatus = EdgegapWindowMetadata.WrapRichTextInColor(
  1080. "Deployed", EdgegapWindowMetadata.StatusColors.Success);
  1081. // Change + Persist read-only fields (ViewDataKeys only save automatically from human input)
  1082. setPersistDeploymentsConnectionUrlLabelTxt(domainWithExternalPort);
  1083. setPersistDeploymentsConnectionStatusLabelTxt(newConnectionStatus);
  1084. // ------------
  1085. // Configure + show stop button
  1086. _deploymentsConnectionStopBtn.clickable.clickedWithEventInfo += onDynamicStopServerBtnAsync; // Unsubscribes on click
  1087. _deploymentsConnectionStopBtn.visible = true;
  1088. _deploymentsConnectionStopBtn.SetEnabled(true);
  1089. // Show refresh btn (currently targeting only this one)
  1090. _deploymentsRefreshBtn.SetEnabled(true);
  1091. }
  1092. private void onCreateDeploymentStartServerFail(EdgegapHttpResult<CreateDeploymentResult> result = null)
  1093. {
  1094. _deploymentsConnectionStatusLabel.text = EdgegapWindowMetadata.WrapRichTextInColor(
  1095. "Failed to Start", EdgegapWindowMetadata.StatusColors.Error);
  1096. _deploymentsStatusLabel.text = EdgegapWindowMetadata.WrapRichTextInColor(
  1097. result?.Error.ErrorMessage ?? "Unknown Error",
  1098. EdgegapWindowMetadata.StatusColors.Error);
  1099. _deploymentsStatusLabel.style.display = DisplayStyle.Flex;
  1100. _deploymentsRefreshBtn.SetEnabled(true);
  1101. Debug.Log("(!) Check your deployments here: https://app.edgegap.com/deployment-management/deployments/list");
  1102. // Shake "need more servers" btn on 403
  1103. // MIRROR CHANGE: use old C# syntax that is supported in Unity 2019
  1104. // bool reachedNumDeploymentsHardcap = result is { IsResultCode403: true };
  1105. bool reachedNumDeploymentsHardcap = result != null && result.IsResultCode403;
  1106. // END MIRROR CHANGE
  1107. if (reachedNumDeploymentsHardcap)
  1108. shakeNeedMoreGameServersBtn();
  1109. }
  1110. /// <summary>
  1111. /// This is triggered from a dynamic button, so we need to pass in the event info (TODO: Use evt info later).
  1112. /// </summary>
  1113. /// <param name="evt"></param>
  1114. private void onDynamicStopServerBtnAsync(EventBase evt) =>
  1115. _ = onDynamicStopServerAsync();
  1116. /// <summary>
  1117. /// Stops the deployment, updating the UI accordingly.
  1118. /// TODO: Cache a list of deployments and/or store a hidden field for requestId.
  1119. /// </summary>
  1120. private async Task onDynamicStopServerAsync()
  1121. {
  1122. // Prepare to stop (UI, status flags, callback unsubs)
  1123. if (IsLogLevelDebug) Debug.Log("onDynamicStopServerAsync");
  1124. hideResultLabels();
  1125. _deploymentsConnectionStopBtn.SetEnabled(false);
  1126. _deploymentsRefreshBtn.SetEnabled(false);
  1127. _deploymentsConnectionStatusLabel.text = EdgegapWindowMetadata.WrapRichTextInColor(
  1128. "<i>Requesting Stop...</i>", EdgegapWindowMetadata.StatusColors.Processing);
  1129. EdgegapDeploymentsApi deployApi = getDeployApi();
  1130. EdgegapHttpResult<StopActiveDeploymentResult> stopResponse = null;
  1131. try
  1132. {
  1133. stopResponse = await deployApi.StopActiveDeploymentAsync(_deploymentRequestId);
  1134. if (!stopResponse.IsResultCode200)
  1135. {
  1136. onDynamicStopServerAsyncFail(stopResponse.Error.ErrorMessage);
  1137. return;
  1138. }
  1139. // ---------
  1140. // 200, but only PENDING deleted (if we create a new one before it's deleted,
  1141. // user may get hit with max # of deployments reached err)
  1142. _deploymentsConnectionStatusLabel.text = EdgegapWindowMetadata.WrapRichTextInColor(
  1143. "<i>Stopping...</i>", EdgegapWindowMetadata.StatusColors.Warn);
  1144. TimeSpan pollIntervalSecs = TimeSpan.FromSeconds(EdgegapWindowMetadata.DEPLOYMENT_STOP_STATUS_POLL_SECONDS);
  1145. stopResponse = await deployApi.AwaitTerminatedDeleteStatusAsync(_deploymentRequestId, pollIntervalSecs);
  1146. }
  1147. finally
  1148. {
  1149. _deploymentsConnectionStopBtn.clickable.clickedWithEventInfo -= onDynamicStopServerBtnAsync;
  1150. }
  1151. bool isStopSuccess = stopResponse.IsResultCode410;
  1152. if (!isStopSuccess)
  1153. {
  1154. onDynamicStopServerAsyncFail(stopResponse.Error.ErrorMessage);
  1155. return;
  1156. }
  1157. // Success: Hide the static row // TODO: Delete the template row, when dynamic
  1158. // clearDeploymentConnections(); // Use this if you don't want to show the last connection info
  1159. string stoppedStr = getConnectionStoppedRichStr();
  1160. _deploymentsStatusLabel.text = ""; // Overrides any previous errs, in case we attempted to created a new deployment while deleting
  1161. setPersistDeploymentsConnectionStatusLabelTxt(stoppedStr);
  1162. }
  1163. private string getConnectionStoppedRichStr() =>
  1164. EdgegapWindowMetadata.WrapRichTextInColor(
  1165. "Stopped", EdgegapWindowMetadata.StatusColors.Error);
  1166. private void onDynamicStopServerAsyncFail(string friendlyErrMsg)
  1167. {
  1168. _deploymentsStatusLabel.text = EdgegapWindowMetadata.WrapRichTextInColor(
  1169. friendlyErrMsg, EdgegapWindowMetadata.StatusColors.Error);
  1170. }
  1171. /// <summary>Sets and returns `_userExternalIp`, prioritizing local cache</summary>
  1172. private async Task<string> getExternalIpAddress()
  1173. {
  1174. if (!string.IsNullOrEmpty(_userExternalIp))
  1175. return _userExternalIp;
  1176. EdgegapIpApi ipApi = getIpApi();
  1177. EdgegapHttpResult<GetYourPublicIpResult> getYourPublicIpResponseTask = await ipApi.GetYourPublicIp();
  1178. _userExternalIp = getYourPublicIpResponseTask?.Data?.PublicIp;
  1179. Assert.IsTrue(!string.IsNullOrEmpty(_userExternalIp),
  1180. $"Expected getYourPublicIpResponseTask.Data.PublicIp");
  1181. return _userExternalIp;
  1182. }
  1183. #region Api Builders
  1184. private EdgegapDeploymentsApi getDeployApi() => new EdgegapDeploymentsApi( // MIRROR CHANGE: 'new()' not supported in Unity 2020
  1185. EdgegapWindowMetadata.API_ENVIRONMENT,
  1186. _apiTokenInput.value.Trim(),
  1187. EdgegapWindowMetadata.LOG_LEVEL);
  1188. private EdgegapIpApi getIpApi() => new EdgegapIpApi( // MIRROR CHANGE: 'new()' not supported in Unity 2020
  1189. EdgegapWindowMetadata.API_ENVIRONMENT,
  1190. _apiTokenInput.value.Trim(),
  1191. EdgegapWindowMetadata.LOG_LEVEL);
  1192. private EdgegapWizardApi getWizardApi() => new EdgegapWizardApi( // MIRROR CHANGE: 'new()' not supported in Unity 2020
  1193. EdgegapWindowMetadata.API_ENVIRONMENT,
  1194. _apiTokenInput.value.Trim(),
  1195. EdgegapWindowMetadata.LOG_LEVEL);
  1196. private EdgegapAppApi getAppApi() => new EdgegapAppApi( // MIRROR CHANGE: 'new()' not supported in Unity 2020
  1197. EdgegapWindowMetadata.API_ENVIRONMENT,
  1198. _apiTokenInput.value.Trim(),
  1199. EdgegapWindowMetadata.LOG_LEVEL);
  1200. #endregion // Api Builders
  1201. private float ProgressCounter = 0;
  1202. // MIRROR CHANGE: added title parameter for more detailed progress while waiting
  1203. void ShowBuildWorkInProgress(string title, string status)
  1204. {
  1205. EditorUtility.DisplayProgressBar(title, status, ProgressCounter++ / 50);
  1206. }
  1207. // END MIRROR CHANGE
  1208. /// <summary>Build & Push - Legacy from v1, modified for v2</summary>
  1209. private async Task buildAndPushServerAsync()
  1210. {
  1211. if (IsLogLevelDebug) Debug.Log("buildAndPushServerAsync");
  1212. // Legacy Code Start >>
  1213. // SetToolUIState(ToolState.Building);
  1214. SyncObjectWithForm();
  1215. ProgressCounter = 0;
  1216. try
  1217. {
  1218. // check for installation and setup docker file
  1219. if (!await EdgegapBuildUtils.DockerSetupAndInstallationCheck())
  1220. {
  1221. onBuildPushError("Docker installation not found. " +
  1222. "Docker can be downloaded from:\n\nhttps://www.docker.com/");
  1223. return;
  1224. }
  1225. // MIRROR CHANGE
  1226. // make sure Linux build target is installed before attemping to build.
  1227. // if it's not installed, tell the user about it.
  1228. if (!BuildPipeline.IsBuildTargetSupported(BuildTargetGroup.Standalone, BuildTarget.StandaloneLinux64))
  1229. {
  1230. 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!");
  1231. return;
  1232. }
  1233. // END MIRROR CHANGE
  1234. if (!EdgegapWindowMetadata.SKIP_SERVER_BUILD_WHEN_PUSHING)
  1235. {
  1236. // create server build
  1237. BuildReport buildResult = EdgegapBuildUtils.BuildServer();
  1238. if (buildResult.summary.result != UnityEditor.Build.Reporting.BuildResult.Succeeded)
  1239. {
  1240. onBuildPushError("Edgegap build failed");
  1241. return;
  1242. }
  1243. }
  1244. else
  1245. Debug.LogWarning(nameof(EdgegapWindowMetadata.SKIP_SERVER_BUILD_WHEN_PUSHING));
  1246. string registry = _containerRegistryUrlInput.value;
  1247. string imageName = _containerImageRepositoryInput.value;
  1248. string tag = _containerNewTagVersionInput.value;
  1249. // MIRROR CHANGE ///////////////////////////////////////////////
  1250. // registry, repository and tag can not contain whitespaces.
  1251. // otherwise the docker command will throw an error:
  1252. // "ERROR: "docker buildx build" requires exactly 1 argument."
  1253. // catch this early and notify the user immediately.
  1254. if (registry.Contains(" "))
  1255. {
  1256. onBuildPushError($"Container Registry is not allowed to contain whitespace: '{registry}'");
  1257. return;
  1258. }
  1259. if (imageName.Contains(" "))
  1260. {
  1261. onBuildPushError($"Image Repository is not allowed to contain whitespace: '{imageName}'");
  1262. return;
  1263. }
  1264. if (tag.Contains(" "))
  1265. {
  1266. onBuildPushError($"Tag is not allowed to contain whitespace: '{tag}'");
  1267. return;
  1268. }
  1269. // END MIRROR CHANGE ///////////////////////////////////////////
  1270. // // increment tag for quicker iteration // TODO? `_autoIncrementTag` !exists in V2.
  1271. // if (_autoIncrementTag)
  1272. // {
  1273. // tag = EdgegapBuildUtils.IncrementTag(tag);
  1274. // }
  1275. // create docker image
  1276. if (!EdgegapWindowMetadata.SKIP_DOCKER_IMAGE_BUILD_WHEN_PUSHING)
  1277. {
  1278. // MIRROR CHANGE: CROSS PLATFORM BUILD SUPPORT
  1279. // await EdgegapBuildUtils.DockerBuild(
  1280. // registry,
  1281. // imageName,
  1282. // tag,
  1283. // ShowBuildWorkInProgress);
  1284. await EdgegapBuildUtils.RunCommand_DockerBuild(registry, imageName, tag, status => ShowBuildWorkInProgress("Building Docker Image", status));
  1285. }
  1286. else
  1287. Debug.LogWarning(nameof(EdgegapWindowMetadata.SKIP_DOCKER_IMAGE_BUILD_WHEN_PUSHING));
  1288. // (v2) Login to registry
  1289. bool isContainerLoginSuccess = await EdgegapBuildUtils.LoginContainerRegistry(
  1290. _containerRegistryUrlInput.value,
  1291. _containerUsernameInput.value,
  1292. _containerTokenInput.value,
  1293. status => ShowBuildWorkInProgress("Logging into container registry.", status)); // MIRROR CHANGE: DETAILED LOGGING
  1294. if (!isContainerLoginSuccess)
  1295. {
  1296. onBuildPushError("Unable to login to docker registry. " +
  1297. "Make sure your registry url + username are correct. " +
  1298. $"See doc:\n\n{EdgegapWindowMetadata.EDGEGAP_DOC_BTN_HOW_TO_LOGIN_VIA_CLI_URL}");
  1299. return;
  1300. }
  1301. // MIRROR CHANGE: DETAILED DOCKER PUSH ERROR HANDLING
  1302. // push docker image
  1303. (bool isPushSuccess, string error) = await EdgegapBuildUtils.RunCommand_DockerPush(registry, imageName, tag, status => ShowBuildWorkInProgress("Uploading Docker Image (this may take a while)", status));
  1304. if (!isPushSuccess)
  1305. {
  1306. // catch common issues with detailed solutions
  1307. if (error.Contains("Cannot connect to the Docker daemon"))
  1308. {
  1309. onBuildPushError($"{error}\nTo solve this, you can install and run Docker Desktop from:\n\nhttps://www.docker.com/products/docker-desktop");
  1310. return;
  1311. }
  1312. if (error.Contains("unauthorized to access repository"))
  1313. {
  1314. onBuildPushError($"Docker authorization failed:\n\n{error}\nTo solve this, you can open a terminal and enter 'docker login {registry}', then enter your credentials.");
  1315. return;
  1316. }
  1317. // project not found?
  1318. if (Regex.IsMatch(error, @".*project .* not found.*", RegexOptions.IgnoreCase))
  1319. {
  1320. 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.");
  1321. return;
  1322. }
  1323. // otherwise show generic error message
  1324. onBuildPushError("Unable to push docker image to registry. " +
  1325. $"Make sure your {registry} registry url + username are correct. " +
  1326. $"See doc:\n\n{EdgegapWindowMetadata.EDGEGAP_DOC_BTN_HOW_TO_LOGIN_VIA_CLI_URL}");
  1327. return;
  1328. }
  1329. // END MIRROR CHANGE
  1330. // update edgegap server settings for new tag
  1331. ShowBuildWorkInProgress("Build and Push", "Updating server info on Edgegap");
  1332. EdgegapAppApi appApi = getAppApi();
  1333. AppPortsData[] ports =
  1334. {
  1335. new AppPortsData() // MIRROR CHANGE: 'new()' not supported in Unity 2020
  1336. {
  1337. Port = int.Parse(_containerPortNumInput.value), // OnInputChange clamps + validates,
  1338. ProtocolStr = _containerTransportTypeEnumInput.value.ToString(),
  1339. },
  1340. };
  1341. UpdateAppVersionRequest updateAppVerReq = new UpdateAppVersionRequest(_appNameInput.value) // MIRROR CHANGE: 'new()' not supported in Unity 2020
  1342. {
  1343. VersionName = _containerNewTagVersionInput.value,
  1344. DockerImage = imageName,
  1345. DockerRepository = registry,
  1346. DockerTag = tag,
  1347. PrivateUsername = _containerUsernameInput.value,
  1348. PrivateToken = _containerTokenInput.value,
  1349. Ports = ports,
  1350. };
  1351. EdgegapHttpResult<UpsertAppVersionResult> updateAppVersionResult = await appApi.UpsertAppVersion(updateAppVerReq);
  1352. if (updateAppVersionResult.HasErr)
  1353. {
  1354. onBuildPushError($"Unable to update docker tag/version:\n{updateAppVersionResult.Error.ErrorMessage}");
  1355. return;
  1356. }
  1357. // cleanup
  1358. onBuildAndPushSuccess(tag);
  1359. }
  1360. catch (Exception ex)
  1361. {
  1362. EditorUtility.ClearProgressBar();
  1363. Debug.LogError(ex);
  1364. string errMsg = "Edgegapbuild and push failed";
  1365. if (ex.Message.Contains("docker daemon is not running"))
  1366. {
  1367. errMsg += ":\nDocker is installed, but the daemon/app (such as `Docker Desktop`) is not running. " +
  1368. "Please start Docker Desktop and try again.";
  1369. }
  1370. else
  1371. errMsg += $":\n{ex.Message}";
  1372. onBuildPushError(errMsg);
  1373. }
  1374. // MIRROR CHANGE: always clear otherwise it gets stuck there forever!
  1375. finally
  1376. {
  1377. EditorUtility.ClearProgressBar();
  1378. }
  1379. // END MIRROR CHANGE
  1380. }
  1381. private void onBuildAndPushSuccess(string tag)
  1382. {
  1383. // _containerImageTag = tag; // TODO?
  1384. syncFormWithObjectStatic();
  1385. EditorUtility.ClearProgressBar();
  1386. _containerBuildAndPushResultLabel.text = $"Success ({tag})";
  1387. _containerBuildAndPushResultLabel.visible = true;
  1388. Debug.Log("Server built and pushed successfully");
  1389. }
  1390. /// <summary>(v2) Docker cmd error, detected by "ERROR" in log stream.</summary>
  1391. private void onBuildPushError(string msg)
  1392. {
  1393. EditorUtility.ClearProgressBar();
  1394. _containerBuildAndPushResultLabel.text = EdgegapWindowMetadata.WrapRichTextInColor(
  1395. "Error", EdgegapWindowMetadata.StatusColors.Error);
  1396. EditorUtility.DisplayDialog("Error", msg, "Ok"); // Show this last! It's blocking!
  1397. }
  1398. #region Persistence Helpers
  1399. /// <summary>
  1400. /// Load from EditorPrefs, persisting from a previous session, if the field is empty
  1401. /// - ApiToken; !persisted via ViewDataKey so we don't save plaintext
  1402. /// - DeploymentRequestId
  1403. /// - DeploymentConnectionUrl
  1404. /// - DeploymentConnectionStatus
  1405. /// </summary>
  1406. private void loadPersistentDataFromEditorPrefs()
  1407. {
  1408. // ApiToken
  1409. if (string.IsNullOrEmpty(_apiTokenInput.value))
  1410. setMaskedApiTokenFromEditorPrefs();
  1411. // DeploymentRequestId
  1412. if (string.IsNullOrEmpty(_deploymentRequestId))
  1413. _deploymentRequestId = EditorPrefs.GetString(EdgegapWindowMetadata.DEPLOYMENT_REQUEST_ID_KEY_STR);
  1414. // DeploymentConnectionUrl
  1415. if (string.IsNullOrEmpty(_deploymentsConnectionUrlReadonlyInput.text))
  1416. {
  1417. _deploymentsConnectionUrlReadonlyInput.value = getDeploymentsConnectionUrlLabelTxt();
  1418. bool hasVal = !string.IsNullOrEmpty(_deploymentsConnectionUrlReadonlyInput.value);
  1419. if (hasVal && string.IsNullOrEmpty(_deploymentRequestId))
  1420. {
  1421. // Fallback -- if no requestId, we can actually get it from the url since we have this (desync)
  1422. _deploymentRequestId = _deploymentsConnectionUrlReadonlyInput.value.Split('.')[0];
  1423. EditorPrefs.SetString(EdgegapWindowMetadata.DEPLOYMENT_REQUEST_ID_KEY_STR, _deploymentRequestId);
  1424. }
  1425. // TODO (Optional): Show a status label to remind these are cached vals; refresh for live?
  1426. }
  1427. // DeploymentConnectionStatus
  1428. if (string.IsNullOrEmpty(_deploymentsConnectionStatusLabel.text) || _deploymentsConnectionStatusLabel.text == "Unknown")
  1429. _deploymentsConnectionStatusLabel.text = getDeploymentsConnectionStatusLabelTxt();
  1430. }
  1431. /// <summary>Set Label -> Persist to EditorPrefs</summary>
  1432. /// <param name="newDomainWithPort"></param>
  1433. private void setPersistDeploymentsConnectionUrlLabelTxt(string newDomainWithPort)
  1434. {
  1435. _deploymentsConnectionUrlReadonlyInput.value = newDomainWithPort;
  1436. EditorPrefs.SetString(EdgegapWindowMetadata.DEPLOYMENT_CONNECTION_URL_KEY_STR, newDomainWithPort);
  1437. }
  1438. /// <summary>Get persistent data fromEditorPrefs</summary>
  1439. private string getDeploymentsConnectionUrlLabelTxt() =>
  1440. EditorPrefs.GetString(EdgegapWindowMetadata.DEPLOYMENT_CONNECTION_URL_KEY_STR);
  1441. /// <summary>Set label -> persist to EditorPrefs</summary>
  1442. /// <param name="newStatus"></param>
  1443. private void setPersistDeploymentsConnectionStatusLabelTxt(string newStatus)
  1444. {
  1445. _deploymentsConnectionStatusLabel.text = newStatus;
  1446. EditorPrefs.SetString(EdgegapWindowMetadata.DEPLOYMENT_CONNECTION_STATUS_KEY_STR, newStatus);
  1447. }
  1448. /// <summary>Get persistent data fromEditorPrefs</summary>
  1449. private string getDeploymentsConnectionStatusLabelTxt() =>
  1450. EditorPrefs.GetString(EdgegapWindowMetadata.DEPLOYMENT_CONNECTION_STATUS_KEY_STR);
  1451. /// <summary>Set to base64 -> Save to EditorPrefs</summary>
  1452. /// <param name="value"></param>
  1453. private void persistUnmaskedApiToken(string value)
  1454. {
  1455. EditorPrefs.SetString(
  1456. EdgegapWindowMetadata.API_TOKEN_KEY_STR,
  1457. Base64Encode(value));
  1458. }
  1459. /// <summary>
  1460. /// Get apiToken from EditorPrefs -> Base64 Decode -> Set to apiTokenInput
  1461. /// </summary>
  1462. private void setMaskedApiTokenFromEditorPrefs()
  1463. {
  1464. string apiTokenBase64Str = EditorPrefs.GetString(
  1465. EdgegapWindowMetadata.API_TOKEN_KEY_STR, null);
  1466. if (apiTokenBase64Str == null)
  1467. return;
  1468. string decodedApiToken = Base64Decode(apiTokenBase64Str);
  1469. _apiTokenInput.SetValueWithoutNotify(decodedApiToken);
  1470. }
  1471. #endregion // Persistence Helpers
  1472. }
  1473. }