// Copyright (c) Wojciech Figat. All rights reserved. using System; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Linq; using FlaxEditor.Content.Settings; using FlaxEditor.CustomEditors; using FlaxEditor.GUI; using FlaxEditor.GUI.Tabs; using FlaxEditor.GUI.Tree; using FlaxEditor.Utilities; using FlaxEngine; using FlaxEngine.GUI; using FlaxEngine.Utilities; using Debug = FlaxEngine.Debug; using PlatformType = FlaxEngine.PlatformType; // ReSharper disable InconsistentNaming // ReSharper disable MemberCanBePrivate.Local #pragma warning disable 649 namespace FlaxEditor.Windows { /// /// Editor tool window for building games using . /// /// public sealed class GameCookerWindow : EditorWindow { /// /// Proxy object for the Build tab. /// [HideInEditor] [CustomEditor(typeof(BuildTabProxy.Editor))] private class BuildTabProxy { public readonly GameCookerWindow GameCookerWin; public readonly PlatformSelector Selector; internal readonly Dictionary PerPlatformOptions = new Dictionary { { PlatformType.Windows, new Windows() }, { PlatformType.XboxOne, new XboxOne() }, { PlatformType.UWP, new UWP() }, { PlatformType.Linux, new Linux() }, { PlatformType.PS4, new PS4() }, { PlatformType.XboxScarlett, new XboxScarlett() }, { PlatformType.Android, new Android() }, { PlatformType.Switch, new Switch() }, { PlatformType.PS5, new PS5() }, { PlatformType.Mac, new Mac() }, { PlatformType.iOS, new iOS() }, }; public BuildTabProxy(GameCookerWindow win, PlatformSelector platformSelector) { GameCookerWin = win; Selector = platformSelector; PerPlatformOptions[PlatformType.Windows].Init("Output/Windows", "Windows"); PerPlatformOptions[PlatformType.XboxOne].Init("Output/XboxOne", "XboxOne"); PerPlatformOptions[PlatformType.UWP].Init("Output/UWP", "UWP"); PerPlatformOptions[PlatformType.Linux].Init("Output/Linux", "Linux"); PerPlatformOptions[PlatformType.PS4].Init("Output/PS4", "PS4"); PerPlatformOptions[PlatformType.XboxScarlett].Init("Output/XboxScarlett", "XboxScarlett"); PerPlatformOptions[PlatformType.Android].Init("Output/Android", "Android"); PerPlatformOptions[PlatformType.Switch].Init("Output/Switch", "Switch"); PerPlatformOptions[PlatformType.PS5].Init("Output/PS5", "PS5"); PerPlatformOptions[PlatformType.Mac].Init("Output/Mac", "Mac"); PerPlatformOptions[PlatformType.iOS].Init("Output/iOS", "iOS"); } [HideInEditor] internal abstract class Platform { [HideInEditor] public bool IsSupported; [HideInEditor] public bool IsAvailable; [EditorOrder(10), Tooltip("Output folder path")] public string Output; [EditorOrder(11), Tooltip("Show output folder in Explorer after build")] public bool ShowOutput = true; [EditorOrder(20), Tooltip("Configuration build mode")] public BuildConfiguration ConfigurationMode = BuildConfiguration.Development; [EditorOrder(90), Tooltip("The list of custom defines passed to the build tool when compiling project scripts. Can be used in build scripts for configuration (Configuration.CustomDefines).")] public string[] CustomDefines; protected abstract BuildPlatform BuildPlatform { get; } protected virtual BuildOptions Options { get { BuildOptions options = BuildOptions.None; if (ShowOutput) options |= BuildOptions.ShowOutput; return options; } } public virtual void Init(string output, string platformDataSubDir) { Output = output; // Check if can build on that platform #if PLATFORM_WINDOWS switch (BuildPlatform) { case BuildPlatform.MacOSx64: case BuildPlatform.MacOSARM64: case BuildPlatform.iOSARM64: IsSupported = false; break; default: IsSupported = true; break; } #elif PLATFORM_LINUX switch (BuildPlatform) { case BuildPlatform.LinuxX64: case BuildPlatform.AndroidARM64: IsSupported = true; break; default: IsSupported = false; break; } #elif PLATFORM_MAC switch (BuildPlatform) { case BuildPlatform.MacOSx64: case BuildPlatform.MacOSARM64: case BuildPlatform.iOSARM64: case BuildPlatform.AndroidARM64: IsSupported = true; break; default: IsSupported = false; break; } #else #error "Unknown platform." #endif // TODO: restore build settings from the Editor cache! // Check if can find installed tools for this platform IsAvailable = Directory.Exists(Path.Combine(Globals.StartupFolder, "Source", "Platforms", platformDataSubDir, "Binaries")); } public virtual void OnNotAvailableLayout(LayoutElementsContainer layout) { string text = "Missing platform data tools for the target platform."; if (FlaxEditor.Editor.IsOfficialBuild()) { switch (BuildPlatform) { #if PLATFORM_WINDOWS case BuildPlatform.Windows32: case BuildPlatform.Windows64: case BuildPlatform.UWPx86: case BuildPlatform.UWPx64: case BuildPlatform.LinuxX64: case BuildPlatform.AndroidARM64: text += "\nUse Flax Launcher and download the required package."; break; #endif default: text += "\nEngine source is required to target this platform."; break; } } else { text += "\nTo target this platform separate engine source package is required."; switch (BuildPlatform) { case BuildPlatform.XboxOne: case BuildPlatform.XboxScarlett: case BuildPlatform.PS4: case BuildPlatform.PS5: case BuildPlatform.Switch: text += "\nTo get access please contact via https://flaxengine.com/contact"; break; } } var label = layout.Label(text, TextAlignment.Center); label.Label.AutoHeight = true; } /// /// Used to add platform specific tools if available. /// /// The layout to start the tools at. public virtual void OnCustomToolsLayout(LayoutElementsContainer layout) { } public virtual void Build() { var output = StringUtils.ConvertRelativePathToAbsolute(Globals.ProjectFolder, StringUtils.NormalizePath(Output)); GameCooker.Build(BuildPlatform, ConfigurationMode, output, Options, CustomDefines); } } class Windows : Platform { protected override BuildPlatform BuildPlatform => BuildPlatform.Windows64; } class UWP : Platform { protected override BuildPlatform BuildPlatform => BuildPlatform.UWPx64; } class XboxOne : Platform { protected override BuildPlatform BuildPlatform => BuildPlatform.XboxOne; } class Linux : Platform { protected override BuildPlatform BuildPlatform => BuildPlatform.LinuxX64; } class PS4 : Platform { protected override BuildPlatform BuildPlatform => BuildPlatform.PS4; } class XboxScarlett : Platform { protected override BuildPlatform BuildPlatform => BuildPlatform.XboxScarlett; } class Android : Platform { protected override BuildPlatform BuildPlatform => BuildPlatform.AndroidARM64; /// public override void OnCustomToolsLayout(LayoutElementsContainer layout) { base.OnCustomToolsLayout(layout); // Add emulation options to android tab. layout.Space(5); var emulatorGroup = layout.Group("Tools"); var sdkPath = Environment.GetEnvironmentVariable("ANDROID_HOME"); if (string.IsNullOrEmpty(sdkPath)) sdkPath = Environment.GetEnvironmentVariable("ANDROID_SDK"); emulatorGroup.Label($"SDK path: {sdkPath}"); // AVD and starting emulator var avdGroup = emulatorGroup.Group("AVD Emulator"); avdGroup.Label("Note: Create AVDs using Android Studio."); avdGroup.Panel.IsClosed = false; var refreshAVDListButton = avdGroup.Button("Refresh AVD list").Button; var avdListGroup = avdGroup.Group("AVD List"); avdListGroup.Panel.IsClosed = false; var noAvdLabel = avdListGroup.Label("No AVDs detected. Click Refresh.", TextAlignment.Center).Label; var avdListTree = new Tree(false) { Parent = avdListGroup.Panel, }; refreshAVDListButton.Clicked += () => { if (avdListTree.Children.Count > 0) avdListTree.DisposeChildren(); var processStartInfo = new System.Diagnostics.ProcessStartInfo { FileName = Path.Combine(sdkPath, "emulator", "emulator.exe"), Arguments = "-list-avds", RedirectStandardOutput = true, CreateNoWindow = true, }; var process = new System.Diagnostics.Process { StartInfo = processStartInfo }; process.Start(); var output = new string(process.StandardOutput.ReadToEnd()); /* CreateProcessSettings processSettings = new CreateProcessSettings { FileName = Path.Combine(sdkPath, "emulator", "emulator.exe"), Arguments = "-list-avds", HiddenWindow = false, SaveOutput = true, WaitForEnd = true, }; //processSettings.ShellExecute = true; FlaxEngine.Platform.CreateProcess(ref processSettings); var output = new string(processSettings.Output);*/ if (output.Length == 0) { noAvdLabel.Visible = true; FlaxEditor.Editor.LogWarning("No AVDs detected."); return; } noAvdLabel.Visible = false; var splitOutput = output.Split('\n'); foreach (var line in splitOutput) { if (string.IsNullOrEmpty(line.Trim())) continue; var item = new TreeNode { Text = line.Trim(), Parent = avdListTree, }; } avdListGroup.Panel.IsClosed = false; }; avdGroup.Label("Emulator AVD Commands:"); var commandsTextBox = avdGroup.TextBox().TextBox; commandsTextBox.IsMultiline = false; commandsTextBox.Text = "-no-snapshot-load -no-boot-anim"; // TODO: save user changes var startEmulatorButton = avdGroup.Button("Start AVD Emulator").Button; startEmulatorButton.TooltipText = "Starts selected AVD from list."; startEmulatorButton.Clicked += () => { if (avdListTree.Selection.Count == 0) return; CreateProcessSettings processSettings = new CreateProcessSettings { FileName = Path.Combine(sdkPath, "emulator", "emulator.exe"), Arguments = $"-avd {avdListTree.Selection[0].Text} {commandsTextBox.Text}", HiddenWindow = true, SaveOutput = false, WaitForEnd = false, }; processSettings.ShellExecute = true; FlaxEngine.Platform.CreateProcess(ref processSettings); }; emulatorGroup.Space(2); // Device var installGroup = emulatorGroup.Group("Install"); installGroup.Panel.IsClosed = false; installGroup.Label("Note: Used to install to AVD or physical devices."); var refreshDeviceListButton = installGroup.Button("Refresh device list").Button; var deviceListGroup = installGroup.Group("List of devices"); deviceListGroup.Panel.IsClosed = false; var noDevicesLabel = deviceListGroup.Label("No devices found. Click Refresh.", TextAlignment.Center).Label; var deviceListTree = new Tree(false) { Parent = deviceListGroup.Panel, }; refreshDeviceListButton.Clicked += () => { if (deviceListTree.Children.Count > 0) deviceListTree.DisposeChildren(); var processStartInfo = new System.Diagnostics.ProcessStartInfo { FileName = Path.Combine(sdkPath, "platform-tools", "adb.exe"), Arguments = "devices -l", RedirectStandardOutput = true, CreateNoWindow = true, }; var process = new System.Diagnostics.Process { StartInfo = processStartInfo }; process.Start(); var output = new string(process.StandardOutput.ReadToEnd()); /* CreateProcessSettings processSettings = new CreateProcessSettings { FileName = Path.Combine(sdkPath, "platform-tools", "adb.exe"), Arguments = "devices -l", HiddenWindow = false, //SaveOutput = true, WaitForEnd = true, }; processSettings.SaveOutput = true; processSettings.ShellExecute = false; FlaxEngine.Platform.CreateProcess(ref processSettings); var output = new string(processSettings.Output); */ if (output.Length > 0 && !output.Equals("List of devices attached", StringComparison.Ordinal)) { noDevicesLabel.Visible = false; var splitLines = output.Split('\n'); foreach (var line in splitLines) { if (line.Trim().Equals("List of devices attached", StringComparison.Ordinal) || string.IsNullOrEmpty(line.Trim())) continue; var tab = line.Split("device "); if (tab.Length < 2) continue; var item = new TreeNode { Text = $"{tab[0].Trim()} - {tab[1].Trim()}", Tag = tab[0].Trim(), Parent = deviceListTree, }; } } else { noDevicesLabel.Visible = true; } deviceListGroup.Panel.IsClosed = false; }; var autoStart = installGroup.Checkbox("Try to auto start activity on device."); var installButton = installGroup.Button("Install APK to Device").Button; installButton.TooltipText = "Installs APK from the output folder to the selected device."; installButton.Clicked += () => { if (deviceListTree.Selection.Count == 0) return; // Get built APK at output path string output = StringUtils.ConvertRelativePathToAbsolute(Globals.ProjectFolder, StringUtils.NormalizePath(Output)); if (!Directory.Exists(output)) { FlaxEditor.Editor.LogWarning("Can not copy APK because output folder does not exist."); return; } var apkFiles = Directory.GetFiles(output, "*.apk"); if (apkFiles.Length == 0) { FlaxEditor.Editor.LogWarning("Can not copy APK because no .apk files were found in output folder."); return; } string apkFilesString = string.Empty; for (int i = 0; i < apkFiles.Length; i++) { var file = apkFiles[i]; if (i == 0) { apkFilesString = $"\"{file}\""; continue; } apkFilesString += $" \"{file}\""; } CreateProcessSettings processSettings = new CreateProcessSettings { FileName = Path.Combine(sdkPath, "platform-tools", "adb.exe"), Arguments = $"-s {deviceListTree.Selection[0].Tag} {(apkFiles.Length > 1 ? "install-multiple" : "install")} {apkFilesString}", LogOutput = true, }; FlaxEngine.Platform.CreateProcess(ref processSettings); if (autoStart.CheckBox.Checked) { var gameSettings = GameSettings.Load(); var productName = gameSettings.ProductName.Replace(" ", "").ToLower(); var companyName = gameSettings.CompanyName.Replace(" ", "").ToLower(); CreateProcessSettings processSettings1 = new CreateProcessSettings { FileName = Path.Combine(sdkPath, "platform-tools", "adb.exe"), Arguments = $"shell am start -n com.{companyName}.{productName}/com.flaxengine.GameActivity", LogOutput = true, }; FlaxEngine.Platform.CreateProcess(ref processSettings1); } }; var adbLogButton = emulatorGroup.Button("Start adb log collecting").Button; adbLogButton.TooltipText = "In debug and development builds the engine and game logs can be output directly to the adb."; adbLogButton.Clicked += () => { var processStartInfo = new System.Diagnostics.ProcessStartInfo { FileName = Path.Combine(sdkPath, "platform-tools", "adb.exe"), Arguments = "logcat Flax:I *:S", CreateNoWindow = false, WindowStyle = ProcessWindowStyle.Normal, }; var process = new System.Diagnostics.Process { StartInfo = processStartInfo }; process.Start(); /* CreateProcessSettings processSettings = new CreateProcessSettings { FileName = Path.Combine(sdkPath, "platform-tools", "adb.exe"), Arguments = $"logcat Flax:I *:S", WaitForEnd = false, }; FlaxEngine.Platform.CreateProcess(ref processSettings); */ }; } } class Switch : Platform { protected override BuildPlatform BuildPlatform => BuildPlatform.Switch; } class PS5 : Platform { protected override BuildPlatform BuildPlatform => BuildPlatform.PS5; } class Mac : Platform { public enum Archs { [EditorDisplay(null, "arm64")] ARM64, [EditorDisplay(null, "x64")] x64, } public Archs CPU = Archs.ARM64; protected override BuildPlatform BuildPlatform => CPU == Archs.ARM64 ? BuildPlatform.MacOSARM64 : BuildPlatform.MacOSx64; } class iOS : Platform { protected override BuildPlatform BuildPlatform => BuildPlatform.iOSARM64; } class Editor : CustomEditor { private PlatformType _platform; private Button _buildButton; public override void Initialize(LayoutElementsContainer layout) { var proxy = (BuildTabProxy)Values[0]; _platform = proxy.Selector.Selected; var platformObj = proxy.PerPlatformOptions[_platform]; if (!platformObj.IsSupported) { layout.Label("This platform is not supported on this system.", TextAlignment.Center); } else if (platformObj.IsAvailable) { string name; switch (_platform) { case PlatformType.Windows: name = "Windows"; break; case PlatformType.XboxOne: name = "Xbox One"; break; case PlatformType.UWP: name = "Windows Store"; layout.Label("UWP (Windows Store) platform has been deprecated and is no longer supported", TextAlignment.Center).Label.TextColor = Color.Red; break; case PlatformType.Linux: name = "Linux"; break; case PlatformType.PS4: name = "PlayStation 4"; break; case PlatformType.XboxScarlett: name = "Xbox Scarlett"; break; case PlatformType.Android: name = "Android"; break; case PlatformType.Switch: name = "Switch"; break; case PlatformType.PS5: name = "PlayStation 5"; break; case PlatformType.Mac: name = "Mac"; break; case PlatformType.iOS: name = "iOS"; break; default: name = Utilities.Utils.GetPropertyNameUI(_platform.ToString()); break; } var group = layout.Group(name); group.Object(new ReadOnlyValueContainer(platformObj)); layout.Space(2); var openOutputButton = layout.Button("Open output folder").Button; openOutputButton.TooltipText = "Opens the defined out folder if the path exists."; openOutputButton.Clicked += () => { string output = StringUtils.ConvertRelativePathToAbsolute(Globals.ProjectFolder, StringUtils.NormalizePath(proxy.PerPlatformOptions[_platform].Output)); if (Directory.Exists(output)) FlaxEngine.FileSystem.ShowFileExplorer(output); else FlaxEditor.Editor.LogWarning($"Can not open path: {output} because it does not exist."); }; layout.Space(2); _buildButton = layout.Button("Build").Button; _buildButton.Clicked += OnBuildClicked; platformObj.OnCustomToolsLayout(layout); } else { platformObj.OnNotAvailableLayout(layout); } } private void OnBuildClicked() { var proxy = (BuildTabProxy)Values[0]; var platformObj = proxy.PerPlatformOptions[_platform]; platformObj.Build(); } public override void Refresh() { base.Refresh(); if (_buildButton != null) { _buildButton.Enabled = !GameCooker.IsRunning; } if (Values.Count > 0 && Values[0] is BuildTabProxy proxy && proxy.Selector.Selected != _platform) { RebuildLayout(); } } } } private class PresetsTargetsColumnBase : ContainerControl { protected GameCookerWindow _cooker; protected PresetsTargetsColumnBase(ContainerControl parent, GameCookerWindow cooker, bool isPresets, Action addClicked) { AnchorPreset = AnchorPresets.VerticalStretchLeft; Parent = parent; Offsets = new Margin(isPresets ? 0 : 140, 140, 0, 0); _cooker = cooker; var title = new Label { Bounds = new Rectangle(0, 0, Width, 19), Text = isPresets ? "Presets" : "Targets", Parent = this, }; var addButton = new Button { Text = isPresets ? "New preset" : "Add target", Bounds = new Rectangle(6, 22, Width - 12, title.Bottom), Parent = this, }; addButton.Clicked += addClicked; } protected void RemoveButtons() { for (int i = ChildrenCount - 1; i >= 0; i--) { if (Children[i].Tag != null) { Children[i].Dispose(); } } } protected void AddButton(string name, int index, int selectedIndex, Action