// 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