using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Reflection; using System.Xml.Linq; using UnityEditor; using UnityEditor.Build.Reporting; using UnityEditor.SceneManagement; using UnityEngine.SceneManagement; using UnityEngine; using game; #if RND_FACEBOOK using Facebook.Unity.Settings; #endif #if UNITY_IOS using UnityEditor.iOS.Xcode; using UnityEngine.iOS; #endif #if PURCHASING_IS_CONNECT using UnityEngine.Purchasing; using UnityEditor.Purchasing; #endif [InitializeOnLoad] public static class BuildUtils { private static readonly string[] _primaryLevels = { "Assets/Scenes/loader.unity", "Assets/Scenes/game.unity" }; private const string BUILD_LOCATION_PATH = "LOCATION_PATH"; [MenuItem("Game/SetUpBuiltInLevels (Editor)", false, 20)] private static void SetUpBuiltInLevels() { string[] levels = GetBuiltInLevels(); var scenes = new List(levels.Length); foreach (string lvl in levels) scenes.Add(new EditorBuildSettingsScene(lvl, true)); EditorBuildSettings.scenes = scenes.ToArray(); } private static string[] GetBuiltInLevels() { const string dir = "Assets/Scenes/Levels"; if (Directory.Exists(dir) == false) { Log.Warn($"Levels directory not found from path {dir}"); return _primaryLevels; } string[] files = Directory.GetFiles(dir).Where(file => file.EndsWith(".unity")).ToArray(); Log.Info($"Founded game levels count: {files?.Length}"); files.ForEach(lvl => Log.Info($"Level add to built in {lvl}")); var levels = new List(_primaryLevels); levels.AddRange(files); return levels.ToArray(); } [MenuItem("Game/BuildAndPlay (Editor)", false, 20)] public static void BuildAndPlay() { if (Build()) PlayGameScene(); } private static void Recompile() { var editorAssembly = Assembly.GetAssembly(typeof(Editor)); Type editorCompilationInterfaceType = editorAssembly.GetType("UnityEditor.Scripting.ScriptCompilation.EditorCompilationInterface"); MethodInfo dirtyAllScriptsMethod = editorCompilationInterfaceType.GetMethod("DirtyAllScripts", BindingFlags.Static | BindingFlags.Public); dirtyAllScriptsMethod.Invoke(editorCompilationInterfaceType, null); } public static void BuildAndPlayRecompile() { Recompile(); BuildAndPlay(); } public static void RecompileAndPlay() { Recompile(); PlayGameScene(); } private static void PlayGameScene() { OpenGameScene(); EditorApplication.isPlaying = true; } private static void OpenGameScene() { Scene scene = SceneManager.GetActiveScene(); if (scene.name != "game") EditorSceneManager.OpenScene("Assets/Scenes/game.unity"); } private static BuildOptions GetBuildOptions() { //var arr = Settings.BUILD_CLIENT_OPTS.Split(','); const BuildOptions opts = new BuildOptions(); //foreach(var name in arr) // opts |= name.ParseEnum(); return opts; } [MenuItem("Game/SetupCommonPlayerSettings (Editor)", false, 20)] private static void SetupCommonPlayerSettings() { PlayerSettings.SetApplicationIdentifier(BuildTargetGroup.Android ,Settings.PACKAGE_ID_FULL); PlayerSettings.SetApplicationIdentifier(BuildTargetGroup.iOS ,Settings.PACKAGE_ID_FULL); PlayerSettings.bundleVersion = Settings.VERSION; PlayerSettings.productName = Settings.CLIENT_APP_NAME; PlayerSettings.companyName = Settings.GAME_COMPANY_NAME; PlayerSettings.stripUnusedMeshComponents = true; #if RND_FACEBOOK FacebookSettings.AppLabels = new List(){Settings.CLIENT_APP_NAME}; FacebookSettings.AppIds = new List(){Settings.FACEBOOK_APP_ID}; FacebookSettings.AndroidKeystorePath = "user-key-store.keystore"; #endif #if PURCHASING_IS_CONNECT PurchasingSettings.enabled = Settings.UNITY_CONNECT_PURCHASING; #endif UpdateIconsForBuildTarget(BuildTargetGroup.Android, Settings.CLIENT_APP_ICON); UpdateIconsForBuildTarget(BuildTargetGroup.iOS, Settings.CLIENT_APP_ICON); } static void UpdateIconsForBuildTarget(BuildTargetGroup target, string icon) { Log.Info($"UpdateIconsForBuildTarget: {target} - {icon}"); int[] iconSizes = PlayerSettings.GetIconSizesForTargetGroup(target); var icons = new Texture2D[iconSizes.Length]; for (var i = 0; i < iconSizes.Length; i++) icons[i] = AssetDatabase.LoadAssetAtPath(icon); PlayerSettings.SetIconsForTargetGroup(target, icons); } private static void EnableIL2CPP(BuildTargetGroup targetGroup, bool flag) { ScriptingImplementation backend = flag ? ScriptingImplementation.IL2CPP : ScriptingImplementation.Mono2x; PlayerSettings.SetScriptingBackend(targetGroup, backend); } //[MenuItem("Game/Build/Android APK + Run %R", false, 21)] public static void BuildAPKAndRun() { BuildAPK(opts => opts.Add(BuildOptions.AutoRunPlayer)); } [MenuItem("Game/Build/Android APK %D", false, 22)] public static void BuildAPKOnly() { BuildAPK(opts => opts.Remove(BuildOptions.AutoRunPlayer)); } private static void BuildAPK(BuildOptionsTweak tweak) { BuildAPK(PrepareBuild(tweak)); } public static void ExportAndroidProject() { ExportAndroidProject(opts => opts.Remove(BuildOptions.AutoRunPlayer)); } private static void ExportAndroidProject(BuildOptionsTweak tweak) { ExportAndroidProject(PrepareBuild(tweak)); } [MenuItem("Game/Build/iOS Project (Clean)", false, 24)] public static void BuildXCodeProjectClean() { BuildXCode(opts => opts .Remove(BuildOptions.AutoRunPlayer) .Remove(BuildOptions.AcceptExternalModificationsToPlayer) ); } private static void BuildXCode(BuildOptionsTweak tweak) { BuildXCode(PrepareBuild(tweak)); } private static void BuildXCode(BuildOptions buildOpts) { PlayerSettings.iOS.allowHTTPDownload = true; PlayerSettings.iOS.targetOSVersionString = "10.0"; PlayerSettings.iOS.sdkVersion = iOSSdkVersion.DeviceSDK; //TODO: simulator build support? #if UNITY_IOS PlayerSettings.iOS.deferSystemGesturesMode = SystemGestureDeferMode.TopEdge | SystemGestureDeferMode.BottomEdge; #endif PlayerSettings.iOS.appleEnableAutomaticSigning = true; const BuildTarget target = BuildTarget.iOS; PlayerSettings.SetUseDefaultGraphicsAPIs(target, automatic: true); PlayerSettings.iOS.appleDeveloperTeamID = Settings.IOS_TEAM_ID; PlayerSettings.iOS.buildNumber = $"{PlayerSettings.bundleVersion}.{Settings.VERSION_REV_NUMBER}"; #if UNITY_IOS //IOSCredentialsPostProcess.ReplaceFirebasePlistAccordingToBundleID(); //TODO: Delete? #endif EnableIL2CPP(BuildTargetGroup.iOS, true); string locationPath = Environment.GetEnvironmentVariable(BUILD_LOCATION_PATH); //TODO: To method? if (string.IsNullOrEmpty(locationPath)) throw new InvalidDataException($"Environment variable {BUILD_LOCATION_PATH} is NULL"); //TODO: With this BuildPipeline.BuildPlayer(GetBuiltInLevels(), locationPath, target, buildOpts); } private static BuildReport BuildStandalone(BuildOptionsTweak tweak) { return BuildStandalone(PrepareBuild(tweak)); } private static void ExportAndroidProject(BuildOptions buildOpts) { var ver = (int)Settings.VERSION_CODE; AndroidArchitecture arch = ParseArch(); int bundleCode = MakeAndroidBundleVersionCode(); Log.Info($"==== GAME VERSION CODE: {ver}, BUNDLE VERSION CODE: {bundleCode} ({arch}) ===="); SetupAndroidBuildSettings(bundleCode, arch); EnableIL2CPP(BuildTargetGroup.Android, Settings.ANDROID_USE_IL2CPP); string locationPath = Environment.GetEnvironmentVariable(BUILD_LOCATION_PATH); if (string.IsNullOrEmpty(locationPath)) throw new InvalidDataException($"Environment variable {BUILD_LOCATION_PATH} is NULL"); BuildPipeline.BuildPlayer(GetBuiltInLevels(), string.Concat(locationPath, ".proj"), BuildTarget.Android, buildOpts); AndroidRepackNativeDebugSymbols(); } private static void BuildAPK(BuildOptions buildOpts) { var ver = (int)Settings.VERSION_CODE; AndroidArchitecture arch = ParseArch(); int bundleCode = MakeAndroidBundleVersionCode(); Log.Info($"==== GAME VERSION CODE: {ver}, BUNDLE VERSION CODE: {bundleCode} ({arch}) ===="); SetupAndroidBuildSettings(bundleCode, arch); EnableIL2CPP(BuildTargetGroup.Android, Settings.ANDROID_USE_IL2CPP); string locationPath = Environment.GetEnvironmentVariable(BUILD_LOCATION_PATH); if (string.IsNullOrEmpty(locationPath)) throw new InvalidDataException($"Environment variable {BUILD_LOCATION_PATH} is NULL"); if ((buildOpts & BuildOptions.AcceptExternalModificationsToPlayer) != 0) locationPath += ".proj"; //EditorUserBuildSettings.androidCreateSymbolsZip = false; //TODO: ??? BuildPipeline.BuildPlayer(GetBuiltInLevels(), locationPath, BuildTarget.Android, buildOpts); AndroidRepackNativeDebugSymbols(); } [MenuItem("Game/SetupAndroidResolverDependencies (Editor)", false, 20)] private static void SetupAndroidResolverDependencies() { const string ardPath = "ProjectSettings/AndroidResolverDependencies.xml"; if (File.Exists(ardPath)) { Log.Info($"Android Resolver Dependencies loading existing file {ardPath}"); XDocument ardDoc = XDocument.Load(ardPath); Log.Info("ARD_doc " + ardDoc .Element("dependencies") .Element("settings") .Elements("setting") .Single(e => e.Attribute("name").Value == "bundleId").Attribute("value").Value ); XElement bundleId = ardDoc .Element("dependencies") .Element("settings") .Elements("setting") .Single(e => e.Attribute("name").Value == "bundleId"); Log.Info($"bundleId {bundleId.Attribute("value").Value}"); bundleId.Attribute("value").Value = Settings.PACKAGE_ID; ardDoc.Save(ardPath); } else { Log.Info($"Android Resolver Dependencies not found {ardPath} file"); } } private static void SetupAndroidBuildSettings(int bundleCode, AndroidArchitecture arch) { PlayerSettings.Android.bundleVersionCode = bundleCode; PlayerSettings.Android.targetArchitectures = arch; PlayerSettings.Android.keyaliasName = "android-build"; PlayerSettings.Android.keyaliasPass = "WV7fx6pRykCk"; PlayerSettings.Android.keystorePass = "WV7fx6pRykCk"; PlayerSettings.Android.minSdkVersion = AndroidSdkVersions.AndroidApiLevel23; PlayerSettings.Android.targetSdkVersion = AndroidSdkVersions.AndroidApiLevelAuto; EditorUserBuildSettings.androidBuildSystem = AndroidBuildSystem.Gradle; EditorUserBuildSettings.exportAsGoogleAndroidProject = false; EditorUserBuildSettings.buildAppBundle = true; SetupAndroidResolverDependencies(); //TODO: ? //EditorUserBuildSettings.androidReleaseMinification = AndroidMinification.Gradle; //EditorUserBuildSettings.androidDebugMinification = AndroidMinification.Gradle; //EditorUserBuildSettings.androidCreateSymbolsZip = Settings.ANDROID_NATIVE_DEBUG_SYMBOLS_ZIP && // Settings.ANDROID_USE_IL2CPP; AndroidSelectTargetStore(); } private static void AndroidSelectTargetStore() { #if PURCHASING_IS_CONNECT switch(Settings.ANDROID_TARGET_STORE) { case EnumAndroidStore.AMAZON: UnityPurchasingEditor.TargetAndroidStore(AppStore.AmazonAppStore); break; case EnumAndroidStore.GPLAY: UnityPurchasingEditor.TargetAndroidStore(AppStore.GooglePlay); break; default: throw new ArgumentNullException($"Unknown Android store flavor: {Settings.ANDROID_TARGET_STORE}"); } #endif } private static string AndroidFlavorFilenameSuffix() { switch(Settings.ANDROID_TARGET_STORE) { case EnumAndroidStore.GPLAY: return "google_play"; default: throw new ArgumentNullException($"Unknown Android store flavor: {Settings.ANDROID_TARGET_STORE}"); } } private static void AndroidRepackNativeDebugSymbols() { if (!EditorUserBuildSettings.androidCreateSymbolsZip) return; string ndkPath = EditorPrefs.GetString("AndroidNdkRootR16b", ""); if (string.IsNullOrEmpty(ndkPath)) { Log.Error("Android NDK path not specified in Unity Editor Preferences - can't repack Android debug symbols"); return; } var prefix = $"{Settings.GAME_PROJECT_NAME}_{AndroidFlavorFilenameSuffix()}"; //E.g.: game_google_play-24.0.0-v3917800.symbols.zip var zipName = $"{prefix}-{PlayerSettings.bundleVersion}-v{PlayerSettings.Android.bundleVersionCode}.symbols.zip"; var outName = $"{prefix}.symbols.zip"; //EditorDevUtils.ProcessGamectlTask($"android_native_debug_symbols_repack {ndk_path} {zip_name} {out_name}"); //TODO: mem } private static BuildReport BuildStandalone(BuildOptions buildOpts) { PlayerSettings.fullScreenMode = FullScreenMode.Windowed; var binName = $"{Settings.GAME_PROJECT_NAME}"; return BuildPipeline.BuildPlayer(GetBuiltInLevels(), binName, BuildTarget.StandaloneOSX, buildOpts); } private static AndroidArchitecture ParseArch() { var architecture = AndroidArchitecture.None; //TODO: mem //string[] arch_strs = Settings.ANDROID_TARGET_ARCHITECTURES.Trim().Split(','); //for(int i = 0; i < arch_strs.Length; i++) // architecture |= arch_strs[i].ParseEnum(); architecture |= AndroidArchitecture.ARM64; architecture |= AndroidArchitecture.ARMv7; return architecture; } private static int MakeAndroidBundleVersionCode() { return (int)(Settings.VERSION_REV_NUMBER * 100); //Last two digits reserved for possible suffixes } private static bool Build() { if (ShouldAskToSaveScene() && !EditorSceneManager.SaveCurrentModifiedScenesIfUserWantsTo()) return false; Log.Info("=== Building Game ==="); return Urun(); } public static bool Urun() { return EditorDevUtils.ProcessGamectlTask("urun"); } private static bool ShouldAskToSaveScene() { return !Application.isPlaying && SceneManager.GetActiveScene().name.IndexOf("game.unity", StringComparison.Ordinal) == -1; } private static BuildOptions Add(this BuildOptions opts, BuildOptions newOpt) { return opts | newOpt; } private static BuildOptions Remove(this BuildOptions opts, BuildOptions uselessOpt) { return opts & ~uselessOpt; } private static BuildOptions PrepareBuild(BuildOptionsTweak tweak) { Build(); SetupCommonPlayerSettings(); return tweak(GetBuildOptions()); } private delegate BuildOptions BuildOptionsTweak(BuildOptions rawOpts); }