Esempio n. 1
0
		public static void RunEtcTool(Bitmap bitmap, AssetBundle bundle, string path, AssetAttributes attributes, bool mipMaps, bool highQualityCompression, byte[] CookingRulesSHA1)
		{
			var hasAlpha = bitmap.HasAlpha;
			var bledBitmap = hasAlpha ? TextureConverterUtils.BleedAlpha(bitmap) : null;
			var ktxPath = Toolbox.GetTempFilePathWithExtension(".ktx");
			var pngPath = Path.ChangeExtension(ktxPath, ".png");
			try {
				(bledBitmap ?? bitmap).SaveTo(pngPath);
				var etcTool = GetToolPath("EtcTool");
				var args =
					$"{pngPath} -format " + (hasAlpha ? "RGBA8" : "ETC1") +
					" -jobs 8 " +
					" -effort " + (highQualityCompression ? "60" : "40") +
					(mipMaps ? " -mipmaps 4" : "") +
					$" -output {ktxPath}";
				if (Process.Start(etcTool, args) != 0) {
					throw new Lime.Exception($"ETCTool error\nCommand line: {etcTool} {args}\"");
				}
				bundle.ImportFile(ktxPath, path, 0, "", attributes, CookingRulesSHA1);
			} finally {
				bledBitmap?.Dispose();
				DeletePossibleLockedFile(pngPath);
				DeletePossibleLockedFile(ktxPath);
			}
		}
Esempio n. 2
0
        public static void RunEtcTool(Bitmap bitmap, AssetBundle bundle, string path, AssetAttributes attributes, bool mipMaps, bool highQualityCompression, byte[] CookingRulesSHA1, DateTime time)
        {
            var hasAlpha   = bitmap.HasAlpha;
            var bledBitmap = hasAlpha ? TextureConverterUtils.BleedAlpha(bitmap) : null;
            var args       = "{0} -format " + (hasAlpha ? "RGBA8" : "RGB8") + " -jobs {1} " +
                             " -effort " + (highQualityCompression ? "60" : "40") +
                             (mipMaps ? " -mipmaps 4" : "") + " -output {2}";
            var hashString = GetTextureHashString(bledBitmap ?? bitmap, ".etc", args);
            var cachePath  = AssetCache.Instance.Load(hashString);

            if (cachePath != null)
            {
                bundle.ImportFile(cachePath, path, 0, "", attributes, time, CookingRulesSHA1);
                return;
            }
            var ktxPath = Toolbox.GetTempFilePathWithExtension(".ktx");
            var pngPath = Path.ChangeExtension(ktxPath, ".png");
            var etcTool = GetToolPath("EtcTool");

            try {
                (bledBitmap ?? bitmap).SaveTo(pngPath);
                if (Process.Start(etcTool, args.Format(pngPath, 8, ktxPath)) != 0)
                {
                    throw new Lime.Exception($"ETCTool error\nCommand line: {etcTool} {args}\"");
                }
                bundle.ImportFile(ktxPath, path, 0, "", attributes, time, CookingRulesSHA1);
                AssetCache.Instance.Save(ktxPath, hashString);
            } finally {
                bledBitmap?.Dispose();
                DeletePossibleLockedFile(pngPath);
                DeletePossibleLockedFile(ktxPath);
            }
        }
Esempio n. 3
0
        public static void ImportTexture(string path, Bitmap texture, ICookingRules rules, byte[] CookingRulesSHA1)
        {
            var textureParamsPath = Path.ChangeExtension(path, ".texture");

            if (!AreTextureParamsDefault(rules))
            {
                UpscaleTextureIfNeeded(ref texture, rules, false);
                var textureParams = new TextureParams {
                    WrapMode  = rules.WrapMode,
                    MinFilter = rules.MinFilter,
                    MagFilter = rules.MagFilter,
                };
                Serialization.WriteObjectToBundle(AssetBundle, textureParamsPath, textureParams, Serialization.Format.Binary, ".texture", AssetAttributes.None, null);
            }
            else
            {
                if (AssetBundle.FileExists(textureParamsPath))
                {
                    DeleteFileFromBundle(textureParamsPath);
                }
            }
            if (ShouldGenerateOpacityMasks())
            {
                var maskPath = Path.ChangeExtension(path, ".mask");
                OpacityMaskCreator.CreateMask(AssetBundle, texture, maskPath);
            }
            var attributes = AssetAttributes.ZippedDeflate;

            if (!TextureConverterUtils.IsPowerOf2(texture.Width) || !TextureConverterUtils.IsPowerOf2(texture.Height))
            {
                attributes |= AssetAttributes.NonPowerOf2Texture;
            }
            switch (Platform)
            {
            case TargetPlatform.Android:
                var f = rules.PVRFormat;
                if (f == PVRFormat.ARGB8 || f == PVRFormat.RGB565 || f == PVRFormat.RGBA4)
                {
                    TextureConverter.RunPVRTexTool(texture, AssetBundle, path, attributes, rules.MipMaps, rules.HighQualityCompression, rules.PVRFormat, CookingRulesSHA1);
                }
                else
                {
                    TextureConverter.RunEtcTool(texture, AssetBundle, path, attributes, rules.MipMaps, rules.HighQualityCompression, CookingRulesSHA1);
                }
                break;

            case TargetPlatform.iOS:
                TextureConverter.RunPVRTexTool(texture, AssetBundle, path, attributes, rules.MipMaps, rules.HighQualityCompression, rules.PVRFormat, CookingRulesSHA1);
                break;

            case TargetPlatform.Win:
            case TargetPlatform.Mac:
                TextureConverter.RunNVCompress(texture, AssetBundle, path, attributes, rules.DDSFormat, rules.MipMaps, CookingRulesSHA1);
                break;

            default:
                throw new Lime.Exception();
            }
        }
Esempio n. 4
0
        public static void RunNVCompress(Bitmap bitmap, AssetBundle bundle, string path, AssetAttributes attributes, DDSFormat format, bool mipMaps, byte[] CookingRulesSHA1, DateTime time)
        {
            bool   compressed = format == DDSFormat.DXTi;
            Bitmap bledBitmap = null;

            if (bitmap.HasAlpha)
            {
                bledBitmap = TextureConverterUtils.BleedAlpha(bitmap);
            }
            string mipsFlag = mipMaps ? string.Empty : "-nomips";
            string compressionMethod;

            if (compressed)
            {
                compressionMethod = bitmap.HasAlpha ? "-bc3" : "-bc1";
            }
            else
            {
#if WIN
                compressionMethod = "-rgb";
#else
                compressionMethod = "-rgb -rgbfmt bgra8";
#endif
            }
            var nvcompress = GetToolPath("nvcompress");

            string args       = "{0} {1}".Format(mipsFlag, compressionMethod);
            var    hashString = GetTextureHashString(bledBitmap ?? bitmap, ".dds", args);
            var    cachePath  = AssetCache.Instance.Load(hashString);
            if (cachePath != null)
            {
                bundle.ImportFile(cachePath, path, 0, "", attributes, time, CookingRulesSHA1);
                return;
            }
            var ddsPath = Toolbox.GetTempFilePathWithExtension(".dds");
            var tgaPath = Path.ChangeExtension(ddsPath, ".tga");
            var srcPath = Path.Combine(Directory.GetCurrentDirectory(), tgaPath);
            var dstPath = Path.Combine(Directory.GetCurrentDirectory(), ddsPath);
            try {
                TextureConverterUtils.SaveToTGA(bledBitmap ?? bitmap, tgaPath, swapRedAndBlue: compressed);
                if (bledBitmap != null && bledBitmap != bitmap)
                {
                    bledBitmap.Dispose();
                }
                args += $" \"{srcPath}\" \"{dstPath}\"";
                if (Process.Start(nvcompress, args.Format(mipsFlag, compressionMethod, srcPath, dstPath), options: Process.Options.RedirectErrors) != 0)
                {
                    throw new Lime.Exception($"NVCompress error\nCommand line: {nvcompress} {args}\"");
                }
                bundle.ImportFile(ddsPath, path, 0, "", attributes, time, CookingRulesSHA1);
                AssetCache.Instance.Save(ddsPath, hashString);
            } finally {
                DeletePossibleLockedFile(ddsPath);
                DeletePossibleLockedFile(tgaPath);
            }
        }
Esempio n. 5
0
        private Size GetTextureSize(string srcPath)
        {
            var  fontPngFile = Path.ChangeExtension(srcPath, ".png");
            Size size;
            bool hasAlpha;

            if (!TextureConverterUtils.GetPngFileInfo(fontPngFile, out size.Width, out size.Height, out hasAlpha, isTangerine))
            {
                throw new Lime.Exception("Font doesn't have an appropriate png texture file");
            }
            return(size);
        }
Esempio n. 6
0
 public static BitmapInfo FromFile(AssetBundle bundle, string path)
 {
     if (TextureConverterUtils.GetPngFileInfo(bundle, path, out var width, out var height, out var hasAlpha))
     {
         return(new BitmapInfo {
             Width = width,
             Height = height,
             HasAlpha = hasAlpha
         });
     }
     Debug.Write($"Failed to read image info {path}");
     return(null);
 }
Esempio n. 7
0
            public static BitmapInfo FromFile(string file)
            {
                int  width;
                int  height;
                bool hasAlpha;

                if (TextureConverterUtils.GetPngFileInfo(file, out width, out height, out hasAlpha, false))
                {
                    return(new BitmapInfo()
                    {
                        Width = width,
                        Height = height,
                        HasAlpha = hasAlpha
                    });
                }
                Debug.Write("Failed to read image info {0}", file);
                return(null);
            }
Esempio n. 8
0
		public static void RunNVCompress(Bitmap bitmap, AssetBundle bundle, string path, AssetAttributes attributes, DDSFormat format, bool mipMaps, byte[] CookingRulesSHA1)
		{
			bool compressed = format == DDSFormat.DXTi;
			Bitmap bledBitmap = null;
			if (bitmap.HasAlpha) {
				bledBitmap = TextureConverterUtils.BleedAlpha(bitmap);
			}
			var ddsPath = Toolbox.GetTempFilePathWithExtension(".dds");
			var tgaPath = Path.ChangeExtension(ddsPath, ".tga");
			try {
				TextureConverterUtils.SaveToTGA(bledBitmap ?? bitmap, tgaPath, swapRedAndBlue: compressed);
				if (bledBitmap != null && bledBitmap != bitmap) {
					bledBitmap.Dispose();
				}
				RunNVCompressHelper(tgaPath, ddsPath, bitmap.HasAlpha, compressed, mipMaps);
				bundle.ImportFile(ddsPath, path, 0, "", attributes, CookingRulesSHA1);
			} finally {
				DeletePossibleLockedFile(ddsPath);
				DeletePossibleLockedFile(tgaPath);
			}
		}
Esempio n. 9
0
        public static void UpscaleTextureIfNeeded(ref Bitmap texture, ICookingRules rules, bool square)
        {
            if (rules.WrapMode == TextureWrapMode.Clamp)
            {
                return;
            }
            if (TextureConverterUtils.IsPowerOf2(texture.Width) && TextureConverterUtils.IsPowerOf2(texture.Height))
            {
                return;
            }
            int newWidth  = CalcUpperPowerOfTwo(texture.Width);
            int newHeight = CalcUpperPowerOfTwo(texture.Height);

            if (square)
            {
                newHeight = newWidth = Math.Max(newWidth, newHeight);
            }
            var newTexture = texture.Rescale(newWidth, newHeight);

            texture.Dispose();
            texture = newTexture;
        }
Esempio n. 10
0
		public static void RunPVRTexTool(Bitmap bitmap, AssetBundle bundle, string path, AssetAttributes attributes, bool mipMaps, bool highQualityCompression, PVRFormat pvrFormat, byte[] CookingRulesSHA1)
		{
			int width = bitmap.Width;
			int height = bitmap.Height;
			bool hasAlpha = bitmap.HasAlpha;

			int potWidth = TextureConverterUtils.GetNearestPowerOf2(width, 8, 2048);
			int potHeight = TextureConverterUtils.GetNearestPowerOf2(height, 8, 2048);
			var args = new StringBuilder();
			switch (pvrFormat) {
				case PVRFormat.PVRTC4:
					if (!hasAlpha) {
						args.Append(" -f PVRTC1_2");
					} else {
						args.Append(" -f PVRTC1_4");
					}
					width = height = Math.Max(potWidth, potHeight);
					break;
				case PVRFormat.PVRTC4_Forced:
					args.Append(" -f PVRTC1_4");
					width = height = Math.Max(potWidth, potHeight);
					break;
				case PVRFormat.PVRTC2:
					args.Append(" -f PVRTC1_2");
					width = height = Math.Max(potWidth, potHeight);
					break;
				case PVRFormat.ETC2:
					args.Append(" -f ETC1 -q etcfast");
					break;
				case PVRFormat.RGB565:
					if (hasAlpha) {
						Console.WriteLine("WARNING: texture has alpha channel. " +
							"Used 'RGBA4444' format instead of 'RGB565'.");
						args.Append(" -f r4g4b4a4 -dither");
					} else {
						args.Append(" -f r5g6b5");
					}
					break;
				case PVRFormat.RGBA4:
					args.Append(" -f r4g4b4a4 -dither");
					break;
				case PVRFormat.ARGB8:
					args.Append(" -f r8g8b8a8");
					break;
			}
			if (highQualityCompression && (new [] { PVRFormat.PVRTC2, PVRFormat.PVRTC4, PVRFormat.PVRTC4_Forced }.Contains (pvrFormat))) {
				args.Append(" -q pvrtcbest");
			}
			var pvrPath = Toolbox.GetTempFilePathWithExtension(".pvr");
			var tgaPath = Path.ChangeExtension(pvrPath, ".tga");
			try {
				if (hasAlpha) {
					bitmap = TextureConverterUtils.BleedAlpha(bitmap);
				}
				TextureConverterUtils.SaveToTGA(bitmap, tgaPath, swapRedAndBlue: true);
				if (mipMaps) {
					args.Append(" -m");
				}
				args.AppendFormat(" -i \"{0}\" -o \"{1}\" -r {2},{3} -shh", tgaPath, pvrPath, width, height);
#if MAC
				var pvrTexTool = GetToolPath("PVRTexTool");
#else
				var pvrTexTool = GetToolPath("PVRTexToolCli");
#endif
				if (Process.Start(pvrTexTool, args.ToString()) != 0) {
					throw new Lime.Exception($"PVRTextTool error\nCommand line: {pvrTexTool} {args}\"");
				}
				bundle.ImportFile(pvrPath, path, 0, "", attributes, CookingRulesSHA1);
			} finally {
				DeletePossibleLockedFile(tgaPath);
				DeletePossibleLockedFile(pvrPath);
			}
		}
Esempio n. 11
0
        public void ImportTexture(string path, Bitmap texture, ICookingRules rules, DateTime time, byte[] CookingRulesSHA1)
        {
            var textureParamsPath = Path.ChangeExtension(path, ".texture");
            var textureParams     = new TextureParams {
                WrapMode  = rules.WrapMode,
                MinFilter = rules.MinFilter,
                MagFilter = rules.MagFilter,
            };

            if (!AreTextureParamsDefault(rules))
            {
                TextureTools.UpscaleTextureIfNeeded(ref texture, rules, false);
                var isNeedToRewriteTexParams = true;
                if (AssetBundle.FileExists(textureParamsPath))
                {
                    var oldTexParams = InternalPersistence.Instance.ReadObject <TextureParams>(textureParamsPath, AssetBundle.OpenFile(textureParamsPath));
                    isNeedToRewriteTexParams = !oldTexParams.Equals(textureParams);
                }
                if (isNeedToRewriteTexParams)
                {
                    InternalPersistence.Instance.WriteObjectToBundle(AssetBundle, textureParamsPath, textureParams, Persistence.Format.Binary, ".texture",
                                                                     File.GetLastWriteTime(textureParamsPath), AssetAttributes.None, null);
                }
            }
            else
            {
                if (AssetBundle.FileExists(textureParamsPath))
                {
                    DeleteFileFromBundle(textureParamsPath);
                }
            }
            if (rules.GenerateOpacityMask)
            {
                var maskPath = Path.ChangeExtension(path, ".mask");
                OpacityMaskCreator.CreateMask(AssetBundle, texture, maskPath);
            }
            var attributes = AssetAttributes.ZippedDeflate;

            if (!TextureConverterUtils.IsPowerOf2(texture.Width) || !TextureConverterUtils.IsPowerOf2(texture.Height))
            {
                attributes |= AssetAttributes.NonPowerOf2Texture;
            }
            switch (Target.Platform)
            {
            case TargetPlatform.Android:
                //case TargetPlatform.iOS:
                var f = rules.PVRFormat;
                if (f == PVRFormat.ARGB8 || f == PVRFormat.RGB565 || f == PVRFormat.RGBA4)
                {
                    TextureConverter.RunPVRTexTool(texture, AssetBundle, path, attributes, rules.MipMaps, rules.HighQualityCompression, rules.PVRFormat, CookingRulesSHA1, time);
                }
                else
                {
                    TextureConverter.RunEtcTool(texture, AssetBundle, path, attributes, rules.MipMaps, rules.HighQualityCompression, CookingRulesSHA1, time);
                }
                break;

            case TargetPlatform.iOS:
                TextureConverter.RunPVRTexTool(texture, AssetBundle, path, attributes, rules.MipMaps, rules.HighQualityCompression, rules.PVRFormat, CookingRulesSHA1, time);
                break;

            case TargetPlatform.Win:
            case TargetPlatform.Mac:
                TextureConverter.RunNVCompress(texture, AssetBundle, path, attributes, rules.DDSFormat, rules.MipMaps, CookingRulesSHA1, time);
                break;

            default:
                throw new Lime.Exception();
            }
        }
Esempio n. 12
0
        public static void AnalyzeResourcesAction()
        {
            var target = The.UI.GetActiveTarget();

            requestedPaths = new List <PathRequestRecord>();
            var crossRefReport           = new List <Tuple <string, List <string> > >();
            var missingResourcesReport   = new List <string>();
            var suspiciousTexturesReport = new List <string>();
            var bundles         = new HashSet <string>();
            var cookingRulesMap = CookingRulesBuilder.Build(AssetBundle.Current, target);

            AssetBundle.Current = new PackedAssetBundle(The.Workspace.GetBundlePath(target.Platform, CookingRulesBuilder.MainBundleName));
            foreach (var i in cookingRulesMap)
            {
                if (i.Key.EndsWith(".png", StringComparison.OrdinalIgnoreCase))
                {
                    if (i.Value.TextureAtlas == null && i.Value.PVRFormat != PVRFormat.PVRTC4 && i.Value.PVRFormat != PVRFormat.PVRTC4_Forced)
                    {
                        suspiciousTexturesReport.Add(string.Format("{0}: {1}, atlas: none",
                                                                   i.Key, i.Value.PVRFormat));
                    }
                    if (i.Value.PVRFormat != PVRFormat.PVRTC4 && i.Value.PVRFormat != PVRFormat.PVRTC4_Forced && i.Value.PVRFormat != PVRFormat.PVRTC2)
                    {
                        int  w;
                        int  h;
                        bool hasAlpha;
                        TextureConverterUtils.GetPngFileInfo(AssetBundle.Current, i.Key, out w, out h, out hasAlpha);
                        if (w >= 1024 || h >= 1024)
                        {
                            suspiciousTexturesReport.Add(string.Format("{3}: {0}, {1}, {2}, {4}, atlas: {5}",
                                                                       w, h, hasAlpha, i.Key, i.Value.PVRFormat, i.Value.TextureAtlas));
                        }
                    }
                }
                foreach (var bundle in i.Value.Bundles)
                {
                    if (bundle != CookingRulesBuilder.MainBundleName)
                    {
                        bundles.Add(bundle);
                    }
                }
            }
            var savedAssetBundle = AssetBundle.Current;

            try {
                var aggregateBundle =
                    new AggregateAssetBundle(
                        bundles.Select(
                            i => new PackedAssetBundle(The.Workspace.GetBundlePath(target.Platform, i))).ToArray());
                AssetBundle.Current = new CustomSetAssetBundle(aggregateBundle,
                                                               aggregateBundle.EnumerateFileInfos().Where(i => {
                    if (cookingRulesMap.TryGetValue(i.Path, out CookingRules rules))
                    {
                        if (rules.Ignore)
                        {
                            return(false);
                        }
                    }
                    return(true);
                }));
                var usedImages = new HashSet <string>();
                var usedSounds = new HashSet <string>();
                foreach (var srcPath in AssetBundle.Current.EnumerateFiles(null, ".tan"))
                {
                    using (var scene = (Frame)Node.CreateFromAssetBundle(srcPath)) {
                        foreach (var j in scene.Descendants)
                        {
                            var checkTexture = new Action <SerializableTexture>((Lime.SerializableTexture t) => {
                                if (t == null)
                                {
                                    return;
                                }
                                string texPath;
                                try {
                                    texPath = t.SerializationPath;
                                } catch {
                                    return;
                                }
                                if (string.IsNullOrEmpty(texPath))
                                {
                                    return;
                                }
                                if (texPath.Length == 2 && texPath[0] == '#')
                                {
                                    switch (texPath[1])
                                    {
                                    case 'a':
                                    case 'b':
                                    case 'c':
                                    case 'd':
                                    case 'e':
                                    case 'f':
                                    case 'g':
                                        return;

                                    default:
                                        suspiciousTexturesReport.Add(string.Format("wrong render target: {0}, {1}", texPath, j.ToString()));
                                        return;
                                    }
                                }
                                string[] possiblePaths = new string[]
                                {
                                    texPath + ".atlasPart",
                                    texPath + ".pvr",
                                    texPath + ".jpg",
                                    texPath + ".png",
                                    texPath + ".dds",
                                    texPath + ".jpg",
                                    texPath + ".png",
                                };
                                foreach (var tpp in possiblePaths)
                                {
                                    if (Lime.AssetBundle.Current.FileExists(tpp))
                                    {
                                        Lime.AssetBundle.Current.OpenFile(tpp);
                                        usedImages.Add(texPath.Replace('\\', '/'));
                                        return;
                                    }
                                }
                                missingResourcesReport.Add(string.Format("texture missing:\n\ttexture path: {0}\n\tscene path: {1}\n",
                                                                         t.SerializationPath, j.ToString()));
                            });
                            var checkAnimators = new Action <Node>((Node n) => {
                                if (n.Animators.TryFind <SerializableTexture>("Texture", out var ta))
                                {
                                    foreach (var key in ta.ReadonlyKeys)
                                    {
                                        checkTexture(key.Value);
                                    }
                                }
                            });
                            if (j is Widget)
                            {
                                var w = j as Lime.Widget;
                                var serializableTexture = w.Texture as SerializableTexture;
                                if (serializableTexture != null)
                                {
                                    checkTexture(serializableTexture);
                                }
                                checkAnimators(w);
                            }
                            else if (j is ParticleModifier)
                            {
                                var pm = j as Lime.ParticleModifier;
                                var serializableTexture = pm.Texture as SerializableTexture;
                                if (serializableTexture != null)
                                {
                                    checkTexture(serializableTexture);
                                }
                                checkAnimators(pm);
                            }
                            else if (j is Lime.Audio)
                            {
                                var au   = j as Lime.Audio;
                                var path = au.Sample.SerializationPath + ".sound";
                                usedSounds.Add(au.Sample.SerializationPath.Replace('\\', '/'));
                                if (!Lime.AssetBundle.Current.FileExists(path))
                                {
                                    missingResourcesReport.Add(string.Format("audio missing:\n\taudio path: {0}\n\tscene path: {1}\n",
                                                                             path, j.ToString()));
                                }
                                else
                                {
                                    using (var tempStream = Lime.AssetBundle.Current.OpenFile(path)) {
                                    }
                                }
                                // FIXME: should we check for audio:Sample animators too?
                            }
                        }
                    }
                    var reportList = new List <string>();
                    foreach (var rpr in requestedPaths)
                    {
                        string pattern = String.Format(@".*[/\\](.*)\.{0}", target.Platform.ToString());
                        string bundle  = "";
                        foreach (Match m in Regex.Matches(rpr.bundle, pattern, RegexOptions.IgnoreCase))
                        {
                            bundle = m.Groups[1].Value;
                        }
                        int index = Array.IndexOf(cookingRulesMap[srcPath].Bundles, bundle);
                        if (index == -1)
                        {
                            reportList.Add(string.Format("\t[{0}]=>[{2}]: {1}",
                                                         string.Join(", ", cookingRulesMap[srcPath].Bundles), rpr.path, bundle));
                        }
                    }
                    requestedPaths.Clear();
                    if (reportList.Count > 0)
                    {
                        crossRefReport.Add(new Tuple <string, List <string> >(srcPath, reportList));
                    }
                    Lime.Application.FreeScheduledActions();
                }

                var allImages = new Dictionary <string, bool>();
                foreach (var img in AssetBundle.Current.EnumerateFiles(null, ".png"))
                {
                    var key = Path.Combine(Path.GetDirectoryName(img), Path.GetFileNameWithoutExtension(img)).Replace('\\', '/');
                    if (!key.StartsWith("Fonts"))
                    {
                        allImages[key] = false;
                    }
                }
                foreach (var img in usedImages)
                {
                    allImages[img] = true;
                }
                var unusedImages = allImages.Where(kv => !kv.Value).Select(kv => kv.Key).ToList();

                var allSounds = new Dictionary <string, bool>();
                foreach (var sound in AssetBundle.Current.EnumerateFiles(null, ".ogg"))
                {
                    var key = Path.Combine(Path.GetDirectoryName(sound), Path.GetFileNameWithoutExtension(sound))
                              .Replace('\\', '/');
                    allSounds[key] = false;
                }
                foreach (var sound in usedSounds)
                {
                    allSounds[sound] = true;
                }
                var unusedSounds = allSounds.Where(kv => !kv.Value).Select(kv => kv.Key).ToList();

                Action <string> writeHeader = (s) => {
                    int n0 = (80 - s.Length) / 2;
                    int n1 = (80 - s.Length) % 2 == 0 ? n0 : n0 - 1;
                    Console.WriteLine("\n" + new String('=', n0) + " " + s + " " + new String('=', n1));
                };
                writeHeader("Cross Bundle Dependencies");
                foreach (var scenePath in crossRefReport)
                {
                    Console.WriteLine("\n" + scenePath.Item1);
                    foreach (var refStr in scenePath.Item2)
                    {
                        Console.WriteLine(refStr);
                    }
                }
                writeHeader("Missing Resources");
                foreach (var s in missingResourcesReport)
                {
                    Console.WriteLine(s);
                }
                writeHeader("Unused Images");
                foreach (var s in unusedImages)
                {
                    Console.WriteLine(s);
                }
                writeHeader("Unused Sounds");
                foreach (var s in unusedSounds)
                {
                    Console.WriteLine(s);
                }
                writeHeader("Suspicious Textures");
                foreach (var s in suspiciousTexturesReport)
                {
                    Console.WriteLine(s);
                }
            } finally {
                AssetBundle.Current = savedAssetBundle;
            }
        }