Пример #1
0
        private void IdentifyCandidate(DumpFormat dumpFormat, ref MatchCandidate mc, Texture2D mcTex)
        {
            if (dumpFormat.usePeixotoCandidateIdentification)
            {
                IdentifyCandidatePeixoto(dumpFormat, ref mc, mcTex);
                return;
            }

            if (mcTex.GetPixel(0, 0).a < 0.01f || mcTex.GetPixel(mcTex.width - 1, 0).a < 0.01f)
            {
                if (dumpFormat.bgParts.Length == 0)
                {
                    mc.bgPartIndex = 1;
                }
                else
                {
                    mc.bgPartIndex = dumpFormat.bgParts.Length;
                }

                mc.isMask = true;
            }
            else
            {
                //TODO - What if there is no part? (Full bg only)
                //TODO - What if different bg parts have the same size? For now, too bad... :)
                if (dumpFormat.bgParts.Length == 0)
                {
                    //Well we already know it's not a mask so we might be able to assume that it's a BG texture, right? ^^'
                    mc.bgPartIndex = -1;
                }
                else
                {
                    for (var j = 0; j < dumpFormat.bgParts.Length; j++)
                    {
                        if (mcTex.width != dumpFormat.bgParts[j].size.x ||
                            mcTex.height != dumpFormat.bgParts[j].size.y)
                        {
                            continue;
                        }

                        mc.bgPartIndex = j;
                        break;
                    }
                }
            }
        }
Пример #2
0
        private void IdentifyCandidatePeixoto(DumpFormat dumpFormat, ref MatchCandidate mc, Texture2D mcTex)
        {
            //As all the textures are squared and the BG ones are 2 256x256 textures,
            // I can quickly check that as an early identification of mask textures.
            if (mcTex.width != 256)
            {
                mc.bgPartIndex = 2;
                mc.isMask      = true;
                return;
            }

            var bottomLeftColors = mcTex.GetPixels(0, 0, 16, 16);
            var avgColor         = bottomLeftColors.GetAverage();
            var isBgPart         = avgColor.Compare(peixotoBgBandColor) >= 0.999f;

            if (isBgPart)
            {
                var topRightColors = mcTex.GetPixels(mcTex.width - 32, mcTex.height - 32, 32, 32);
                avgColor = topRightColors.GetAverage();
                var isRightBgPart = avgColor.Compare(peixotoRightBgColor) >= 0.999f;

                mc.isMask      = false;
                mc.bgPartIndex = isRightBgPart ? 1 : 0;
            }
            else
            {
                mc.bgPartIndex = 2;
                mc.isMask      = true;
            }

            //sRGB or Linear doesn't seem to make any difference for these old assets.
            // Let's keep the unity default, sRGB.

            // var name = Path.GetFileNameWithoutExtension(fm.fileInfos[mc.fileInfoIndices[0]].Name);
            // var testSrgb = new Texture2D(16, 16, TextureFormat.RGBA32, false, false);
            // testSrgb.SetPixels(bottomLeftColors);
            // fm.SaveTextureToPng(testSrgb, "./", $"{name}_srgb");
            // Object.Destroy(testSrgb);

            // var testLinear = new Texture2D(16, 16, TextureFormat.RGBA32, false, true);
            // testLinear.SetPixels(bottomLeftColors);
            // fm.SaveTextureToPng(testSrgb, "./", $"{name}_linear");
            // Object.Destroy(testLinear);
        }
        public IEnumerator RecreateTextures(string processedPath, string bgInfoPath, string alphaChannelPath,
                                            string resultsPath, DumpFormat dumpFormat, System.Action <ProgressInfo> progressCb, System.Action doneCb)
        {
            reportSb.Clear();
            reportSb.AppendLine("== Texture recreation report ==");
            reportSb.AppendLine("== Textures recreation Started! ==");
            reportSb.AppendLine("Selected Format: " + dumpFormat.name);

            progressCb(new ProgressInfo("Loading Bg Infos", 0, 0, 0f));
            yield return(new WaitForEndOfFrame());

            yield return(new WaitForEndOfFrame());

            taskTime = Time.unscaledTime;

            processedPath    = Path.Combine(processedPath, game.ToString());
            alphaChannelPath = Path.Combine(alphaChannelPath, game.ToString());
            bgInfoPath       = Path.Combine(bgInfoPath, game.ToString());

            resultsPath = Path.Combine(resultsPath, dumpFormat.name);
            fm.CreateDirectory(resultsPath);

            var bgInfosCount = fm.LoadFiles(bgInfoPath, "json");

            if (bgInfosCount <= 0)
            {
                reportSb.AppendLine("== Texture recreation aborted - No Bg Info ==");
                doneCb();
                yield break;
            }

            var bgInfos = fm.GetObjectsFromFiles <BgInfo>();

            var processedTexCount = fm.LoadFiles(processedPath, "png", SearchOption.AllDirectories);

            if (processedTexCount <= 0)
            {
                reportSb.AppendLine("== Texture recreation aborted - No Processed Texture ==");
                doneCb();
                yield break;
            }

            progressCb(new ProgressInfo(string.Concat("Recreating Textures - ", bgInfos[0].namePrefix), 1, bgInfosCount,
                                        0 / (float)(bgInfosCount - 1)));
            yield return(new WaitForEndOfFrame());

            yield return(new WaitForEndOfFrame());

            for (var i = 0; i < bgInfosCount; i++)
            {
                var bgInfo = bgInfos[i];

                //Get the right processed background
                var expectedBgName = string.Concat(bgInfo.namePrefix, ".png");
                var bgTexFileInfo  = fm.fileInfos.FirstOrDefault(x => x.Name == expectedBgName);
                if (bgTexFileInfo == null)
                {
                    reportSb.AppendLine(string.Concat("Missing upscaled BG texture: ", bgInfo.namePrefix));
                    continue;
                }

                progressCb(new ProgressInfo(string.Concat("Recreating Textures - ", bgInfo.namePrefix), i + 1,
                                            bgInfosCount, i / (float)(bgInfosCount - 1)));
                yield return(new WaitForEndOfFrame());

                Texture2D processedTexAms = null;
                var       processedTex    = fm.GetTextureFromFileInfo(bgTexFileInfo);

                //float texRatioFloat = processedTex.width / (float)bgInfo.bgTexSize.x;
                var bgRatioFloat   = processedTex.width / (float)bgInfo.bgTexSize.x;
                var maskRatioFloat = processedTex.width / (float)baseDumpFormat.maskUsageSize.x;

                if (bgRatioFloat - Mathf.Floor(bgRatioFloat) != 0f ||
                    maskRatioFloat - Mathf.Floor(maskRatioFloat) != 0f)
                {
                    reportSb.AppendLine(string.Concat(
                                            "Error: This tool is not compatible with non integer scaling. Please fix this processed texture:",
                                            processedTex.name));
                    Object.Destroy(processedTex);
                    continue;
                }

                var bgRatio   = Mathf.RoundToInt(bgRatioFloat);
                var maskRatio = Mathf.RoundToInt(maskRatioFloat);

                CompensatePixelShift(dumpFormat.texPixelShift, processedTex, maskRatio);

                //Dynamic bg size only works for full background with no parts then... it's not good.
                //I really hope Resident evil 3 or something doesn't have backgrounds texture with different size AND splitted in some bullshit way.
                //If so I will need to analyze the background files during the matching phase and save all that data in the BG info instead.

                BgTexturePart[] bgParts;
                if (dumpFormat.bgParts == null || dumpFormat.bgParts.Length == 0)
                {
                    var fullBgPart = new BgTexturePart();
                    fullBgPart.size    = new Vector2Int(bgInfo.bgTexSize.x, bgInfo.bgTexSize.y);
                    fullBgPart.patches = new[] { new Patch(0, 0, 0, 0, bgInfo.bgTexSize.x, bgInfo.bgTexSize.y) };
                    bgParts            = new[] { fullBgPart };
                }
                else
                {
                    bgParts = dumpFormat.bgParts;
                }

                DumpMatch dumpMatch;
                if (bgInfo.texDumpMatches.Count(x => x.formatName == dumpFormat.name) <= 0)
                {
                    dumpMatch = bgInfo.texDumpMatches.FirstOrDefault(
                        x => x.formatName == dumpFormat.alternateFormatName);
                }
                else
                {
                    dumpMatch = bgInfo.texDumpMatches.FirstOrDefault(x => x.formatName == dumpFormat.name);
                }

                if (string.IsNullOrEmpty(dumpMatch.formatName))
                {
                    Object.Destroy(processedTex);
                    continue;
                }

                var matchGroupTexCount = bgParts.Length + (bgInfo.hasMask ? 1 : 0);
                //int matchGroupCount = dumpMatch.texNames.Length / matchGroupTexCount;

                //progressCb(new ProgressInfo(string.Concat(bgInfo.namePrefix, " - Recreating BG textures"), i + 1, bgInfosCount, i / (float)(bgInfosCount - 1)));
                //yield return new WaitForEndOfFrame();

                //Generate the Bg textures
                for (var j = 0; j < bgParts.Length; j++)
                {
                    var bgPartTex = new Texture2D(
                        Mathf.RoundToInt(bgParts[j].size.x * bgRatio),
                        Mathf.RoundToInt(bgParts[j].size.y * bgRatio),
                        TextureFormat.RGBA32, false);

                    bgPartTex.Fill(new Color32(0, 0, 0, 255));

                    for (var k = 0; k < bgParts[j].patches.Length; k++)
                    {
                        var p = bgParts[j].patches[k];
                        p.Scale(bgRatio);

                        var pColors = processedTex.GetPixels(
                            p.dstPos.x, p.dstPos.y,
                            p.size.x, p.size.y);

                        if (bgParts[j].needGapCompensation && k == 1)
                        {
                            bgPartTex.SetPixels(
                                p.srcPos.x, p.srcPos.y + 1,
                                p.size.x, p.size.y, pColors);
                        }
                        else
                        {
                            bgPartTex.SetPixels(
                                p.srcPos.x, p.srcPos.y,
                                p.size.x, p.size.y, pColors);
                        }
                    }

                    for (var k = 0; k < dumpMatch.partIndices.Length; k++)
                    {
                        if (dumpMatch.partIndices[k] != j)
                        {
                            continue;
                        }

                        switch (dumpFormat.BgFormat)
                        {
                        case ImageFormat.Png:
                            fm.SaveTextureToPng(bgPartTex, resultsPath, dumpMatch.texNames[k]);
                            break;

                        case ImageFormat.Jpg:
                            fm.SaveTextureToJPG(bgPartTex, resultsPath, dumpMatch.texNames[k],
                                                dumpFormat.jpgQuality);
                            break;

                        case ImageFormat.DdsBc7:
                            fm.SaveTextureToDds(bgPartTex, resultsPath, dumpMatch.texNames[k]);
                            break;

                        case ImageFormat.DdsBc3:
                            fm.SaveTextureToDds(bgPartTex, resultsPath, dumpMatch.texNames[k]);
                            break;
                        }
                    }

                    Object.Destroy(bgPartTex);
                }

                //2. Generate the mask texture
                if (bgInfo.hasMask)
                {
                    //progressCb(new ProgressInfo(string.Concat(bgInfo.namePrefix, " - Recreating mask"), i+1, bgInfosCount, i / (float)(bgInfosCount-1)));
                    //yield return new WaitForEndOfFrame();

                    //One per group of masks
                    var alphaChannels           = new Texture2D[bgInfo.groupsCount];
                    var hasMissingAlphaChannels = false;
                    for (var g = 0; g < bgInfo.groupsCount; g++)
                    {
                        alphaChannels[g] = fm.GetTextureFromPath(Path.Combine(alphaChannelPath,
                                                                              string.Concat(bgInfo.namePrefix, "_", g)));

                        if (alphaChannels[g] != null)
                        {
                            continue;
                        }

                        reportSb.AppendLine(string.Concat(bgInfo.namePrefix,
                                                          " is missing the alpha channel texture ", g, "."));

                        hasMissingAlphaChannels = true;
                    }

                    if (hasMissingAlphaChannels)
                    {
                        Object.Destroy(processedTex);
                        reportSb.AppendLine(string.Concat("Missing alpha channel textures: ", bgInfo.namePrefix));
                        continue;
                    }

                    if (bgInfo.useProcessedMaskTex)
                    {
                        //Object.Destroy(processedTex);
                        processedTexAms = fm.GetTextureFromPath(Path.Combine(processedPath,
                                                                             string.Concat(bgInfo.namePrefix, altMaskSourceSuffix)));
                        if (processedTexAms == null)
                        {
                            processedTexAms = fm.GetTextureFromPath(Path.Combine(processedPath, "AMS",
                                                                                 string.Concat(bgInfo.namePrefix, altMaskSourceSuffix)));
                            if (processedTexAms == null)
                            {
                                reportSb.AppendLine(string.Concat("Missing special mask source textures: ",
                                                                  bgInfo.namePrefix));
                                continue;
                            }
                        }

                        CompensatePixelShift(dumpFormat.texPixelShift, processedTexAms, maskRatio);
                    }

                    //Reconstruct the mask texture itself based on processed BG and the smoothed alpha texture
                    var texWidth = Mathf.RoundToInt((dumpFormat.maskForcedSize.x != 0
                        ? dumpFormat.maskForcedSize.x
                        : bgInfo.maskTexSize.x) * maskRatio);

                    var texHeight = Mathf.RoundToInt((dumpFormat.maskForcedSize.x != 0
                        ? dumpFormat.maskForcedSize.y
                        : bgInfo.maskTexSize.y) * maskRatio);

                    var maskTex = new Texture2D(texWidth, texHeight, TextureFormat.RGBA32, false);
                    maskTex.Fill(new Color32());

                    Texture2D alternateMaskTex = null;
                    if (dumpFormat.isMonochromaticMask && bgInfo.useProcessedMaskTex)
                    {
                        alternateMaskTex = new Texture2D(texWidth, texHeight, TextureFormat.RGBA32, false);
                        alternateMaskTex.Fill(new Color32());
                    }


                    for (var j = 0; j < bgInfo.masks.Length; j++)
                    {
                        var mask = bgInfo.masks[j];
                        mask.patch.Scale(maskRatio);

                        Color[] pColors;
                        var     isAltMaskPatch = false;

                        if (bgInfo.useProcessedMaskTex == false || mask.ignoreAltMaskSource)
                        {
                            if (dumpFormat.isMonochromaticMask)
                            {
                                pColors = new Color[mask.patch.size.x * mask.patch.size.y];
                                for (var k = 0; k < pColors.Length; k++)
                                {
                                    pColors[k] = Color.white;
                                }
                            }
                            else
                            {
                                pColors = processedTex.GetPixels(mask.patch.dstPos.x, mask.patch.dstPos.y,
                                                                 mask.patch.size.x, mask.patch.size.y);
                            }
                        }
                        else
                        {
                            if (!dumpFormat.isMonochromaticMask)
                            {
                                pColors = processedTexAms.GetPixels(mask.patch.dstPos.x, mask.patch.dstPos.y,
                                                                    mask.patch.size.x, mask.patch.size.y);
                            }
                            else
                            {
                                var amsHistogram  = new Histogram(3, 16, 1f);
                                var origHistogram = new Histogram(3, 16, 1f);

                                pColors = processedTex.GetPixels(mask.patch.dstPos.x, mask.patch.dstPos.y,
                                                                 mask.patch.size.x, mask.patch.size.y);

                                origHistogram.AddValues(pColors);

                                pColors = processedTexAms.GetPixels(mask.patch.dstPos.x, mask.patch.dstPos.y,
                                                                    mask.patch.size.x, mask.patch.size.y);

                                amsHistogram.AddValues(pColors);

                                var histogramMatchValue = amsHistogram.Compare(origHistogram);
                                isAltMaskPatch = histogramMatchValue <
                                                 dumpFormat.monoMaskAmsHistogramMinMatchValue - Mathf.Epsilon;

                                Debug.Log($"{bgInfo.namePrefix}_mask_{j}_{histogramMatchValue:0.00000}");

                                if (!isAltMaskPatch)
                                {
                                    for (var k = 0; k < pColors.Length; k++)
                                    {
                                        pColors[k] = Color.white;
                                    }
                                }
                                else
                                {
                                    var amsTestTex = new Texture2D(mask.patch.size.x, mask.patch.size.y,
                                                                   TextureFormat.RGBA32, false);

                                    amsTestTex.SetPixels(pColors);
                                    fm.SaveTextureToPng(amsTestTex, resultsPath,
                                                        $"{bgInfo.namePrefix}_mask_{j}_{histogramMatchValue}");
                                }
                            }
                        }

                        var aColors = alphaChannels[mask.groupIndex].GetPixels(mask.patch.dstPos.x, mask.patch.dstPos.y,
                                                                               mask.patch.size.x, mask.patch.size.y);

                        for (var k = 0; k < pColors.Length; k++)
                        {
                            pColors[k].a = aColors[k].a;
                        }

                        var srcPosY = mask.patch.srcPos.y + (maskTex.height - bgInfo.maskTexSize.y * maskRatio);

                        if (isAltMaskPatch)
                        {
                            alternateMaskTex.SetPixels(mask.patch.srcPos.x, srcPosY, mask.patch.size.x,
                                                       mask.patch.size.y,
                                                       pColors);
                        }
                        else
                        {
                            maskTex.SetPixels(mask.patch.srcPos.x, srcPosY, mask.patch.size.x, mask.patch.size.y,
                                              pColors);
                        }
                    }

                    for (var j = 0; j < dumpMatch.partIndices.Length; j++)
                    {
                        if (dumpMatch.partIndices[j] == matchGroupTexCount - 1)
                        {
                            if (alternateMaskTex != null)
                            {
                                if (dumpFormat.MaskFormat == ImageFormat.Png)
                                {
                                    fm.SaveTextureToPng(alternateMaskTex, resultsPath, $"{dumpMatch.texNames[j]}_alt");
                                }
                                else
                                {
                                    fm.SaveTextureToDds(alternateMaskTex, resultsPath, $"{dumpMatch.texNames[j]}_alt");
                                }
                            }

                            if (dumpFormat.MaskFormat == ImageFormat.Png)
                            {
                                fm.SaveTextureToPng(maskTex, resultsPath, dumpMatch.texNames[j]);
                            }
                            else
                            {
                                fm.SaveTextureToDds(maskTex, resultsPath, dumpMatch.texNames[j]);
                            }
                        }
                    }

                    //Clean up the mess :)
                    //(and yes don't reuse the texture object, since other games might need dynamic texture size
                    //AND texture.resize IS a hidden constructor too)
                    Object.Destroy(maskTex);
                    for (var g = 0; g < bgInfo.groupsCount; g++)
                    {
                        Object.Destroy(alphaChannels[g]);
                    }
                }

                Object.Destroy(processedTex);
                Object.Destroy(processedTexAms);
            }

            yield return(new WaitForEndOfFrame());

            reportSb.AppendLine(string.Format("== Textures recreation Done! ({0} seconds) ==",
                                              (Time.unscaledTime - taskTime).ToString("#.0")));

            fm.OpenFolder(resultsPath);

            doneCb();
        }
Пример #4
0
        public IEnumerator MatchTextures(string bgInfoPath, string dumpTexPath, DumpFormat dumpFormat,
                                         TextureMatchingConfig config, System.Action <ProgressInfo> progressCb, System.Action doneCb)
        {
            if (dumpFormat.Equals(baseDumpFormat))
            {
                Debug.LogWarning(string.Format(
                                     "The current base texture format ({0}) is the same as the selected texture format for matching ({1}). This is useless.",
                                     baseDumpFormat.name, dumpFormat.name));
                doneCb();
                yield break;
            }

            reportSb.Clear();
            reportSb.AppendLine("== Texture Matching report ==");
            reportSb.AppendLine("== Texture Matching Started! ==");
            reportSb.AppendLine("Selected Format: " + dumpFormat.name);

            taskTime = Time.unscaledTime;

            progressCb(new ProgressInfo("Loading textures and BgInfos", 0, 0, 0f));
            yield return(new WaitForEndOfFrame());

            yield return(new WaitForEndOfFrame());

            //Load all the bg infos for later
            bgInfoPath = Path.Combine(bgInfoPath, game.ToString());
            var bgInfoCount = fm.LoadFiles(bgInfoPath, "json");
            var bgInfos     = fm.GetObjectsFromFiles <BgInfo>();

            //Load all the file info of the textures to match
            var mcTexCount = fm.LoadFiles(Path.Combine(dumpTexPath, dumpFormat.name), "png",
                                          SearchOption.AllDirectories);

            var candidatesList = new List <MatchCandidate>();

            var texDuplicatesCount = 0;
            var unmatchedTexCount  = 0;
            var unmatchedMcCount   = 0;

            //1. Prepare the MatchCandidates
            for (var i = 0; i < mcTexCount; i++)
            {
                if (i % 50 == 0)
                {
                    progressCb(
                        new ProgressInfo("Preparing candidates", i + 1, mcTexCount, i / (float)(mcTexCount - 1)));
                    yield return(new WaitForEndOfFrame());
                }

                var mcTex = fm.GetTextureFromFileIndex(i);

                var mc = new MatchCandidate();

                mc.md5              = fm.GetMd5(mcTex.GetRawTextureData());
                mc.fileInfoIndices  = new int[] { i };
                mc.texSize.x        = mcTex.width;
                mc.texSize.y        = mcTex.height;
                mc.bgInfoMatchIndex = new List <int>();
                mc.bgInfoMatchValue = new List <float>();

                //Check for duplicates
                var isDuplicate = false;
                for (var j = 0; j < candidatesList.Count; j++)
                {
                    if (candidatesList[j].md5 == mc.md5)
                    {
                        reportSb.AppendLine(string.Concat(mcTex.name, " is a duplicate of ",
                                                          fm.fileInfos[candidatesList[j].fileInfoIndices[0]].Name));
                        texDuplicatesCount++;

                        //int[] indices = new int[candidatesList[j].fileInfoIndices.Length + 1];
                        //for (int k = 0; k < candidatesList[j].fileInfoIndices.Length; k++)
                        //{
                        //    indices[k] = candidatesList[j].fileInfoIndices[k];
                        //}
                        //indices[indices.Length - 1] = j;
                        var indices = candidatesList[j].fileInfoIndices.Concat(new int[] { i }).ToArray();

                        var duplicatedMc = candidatesList[j];
                        duplicatedMc.SetFileInfoIndices(indices);
                        candidatesList[j] = duplicatedMc;

                        isDuplicate = true;

                        //It can't have another duplicate, impossible since I checked for every new element.
                        break;
                    }
                }

                if (isDuplicate)
                {
                    continue;
                }

                //Identify the candidate
                IdentifyCandidate(dumpFormat, ref mc, mcTex);

                //RE3 WARNING - That doesn't WORK AT ALL ON RE3 GAMECUBE
                //Create a special patch for mask texture, thus It will only pick colors into the non fully transparent area.
                var partPatch = new Patch();
                if (mc.isMask)
                {
                    //For some dumping format like Peixoto Before, starting the matching process,
                    // transparency needs to be added by replacing pure black pixels with transparent ones.
                    if (dumpFormat.useBlackAsTransparent)
                    {
                        var colors = mcTex.GetPixels32();
                        for (int j = 0; j < colors.Length; j++)
                        {
                            var c = colors[j];
                            if (c.r + c.g + c.b <= byte.MinValue)
                            {
                                c.a       = byte.MinValue;
                                colors[j] = c;
                            }
                        }

                        mcTex.SetPixels32(colors);
                        mcTex.Apply();

                        // Save the texture with proper transparent pixels.
                        // var fi = fm.fileInfos[i];
                        // Debug.Log(fi.FullName);
                        // var testName = $"{Path.GetFileNameWithoutExtension(fi.Name)}";
                        // fm.SaveTextureToPng(mcTex, fi.DirectoryName, testName);
                    }

                    if (config.inconsistentMaskSize)
                    {
                        partPatch = new Patch(0, mcTex.height - 23, 0, mcTex.height - 23, 128, 23);
                    }
                    else
                    {
                        var line      = mcTex.GetPixels(12, 0, 1, mcTex.height);
                        var foundFlag = false;
                        for (var k = 0; k < line.Length; k++)
                        {
                            if (line[k].a > 0.9f)
                            {
                                partPatch.srcPos.y = k;
                                partPatch.dstPos.y = k;
                                partPatch.size.y   = mcTex.height - k;
                                foundFlag          = true;
                                break;
                            }
                        }

                        if (foundFlag == false || partPatch.size.y < config.histogramPatchSize.y)
                        {
                            unmatchedMcCount++;
                            unmatchedTexCount += mc.fileInfoIndices.Length;
                            reportSb.AppendLine(string.Concat(mcTex.name,
                                                              " seems fully transparent mask. Match it manually."));
                            continue;
                        }

                        line      = mcTex.GetPixels(0, mcTex.height - 4, mcTex.width, 1);
                        foundFlag = false;
                        for (var k = mcTex.width - 1; k >= 0; k--)
                        {
                            if (line[k].a > 0.9f)
                            {
                                partPatch.size.x = k + 1;
                                foundFlag        = true;
                                break;
                            }
                        }

                        if (foundFlag == false || partPatch.size.x < config.histogramPatchSize.x)
                        {
                            unmatchedMcCount++;
                            unmatchedTexCount += mc.fileInfoIndices.Length;
                            reportSb.AppendLine(string.Concat(mcTex.name,
                                                              " seems fully transparent mask. Match it manually."));
                            continue;
                        }
                    }
                }

                //Generate the histogram data
                var histGenAttemptsCount = 0;
                var isMonochromatic      = false;

                mc.histograms  = new Histogram[config.histogramPatchCount];
                mc.HistPatches = new Patch[config.histogramPatchCount];
                //mc.histBgPartPatchIndices = new int[config.histogramPatchCount];

                for (var j = 0; j < config.histogramPatchCount; j++)
                {
                    if (mc.isMask == false)
                    {
                        if (mc.bgPartIndex != -1)
                        {
                            var bgPartPatchIndex = Random.Range(0, dumpFormat.bgParts[mc.bgPartIndex].patches.Length);
                            partPatch = dumpFormat.bgParts[mc.bgPartIndex].patches[bgPartPatchIndex];
                        }
                        else
                        {
                            partPatch = new Patch(0, 0, 0, 0, mc.texSize.x, mc.texSize.y);
                        }
                    }

                    var p = partPatch;

                    p.size = config.histogramPatchSize;

                    var patchPos = partPatch.size - config.histogramPatchSize;
                    patchPos.x = Random.Range(0, patchPos.x);
                    patchPos.y = Random.Range(0, patchPos.y);

                    p.Move(patchPos);

                    var histogram = new Histogram(mc.isMask ? 4 : 3, config.histogramStepCount, 1f);

                    var patchColors = mcTex.GetPixels(p.srcPos.x, p.srcPos.y, p.size.x, p.size.y);

                    histogram.AddValues(patchColors, mc.isMask);

                    //Compare the new histogram with the previous one, if there are the same the texture might be monochromatic...
                    if (j > 0)
                    {
                        if (histogram.Compare(mc.histograms[mc.histograms.Length - 1]) >= 0.999f)
                        {
                            histGenAttemptsCount++;

                            if (histGenAttemptsCount > config.histGenAttemptsMaxCount)
                            {
                                isMonochromatic = true;
                                break;
                            }

                            j--;
                            continue;
                        }
                    }

                    if (config.savePatchTexures)
                    {
                        var test = new Texture2D(p.size.x, p.size.y);
                        test.SetPixels(patchColors);
                        fm.SaveTextureToPng(test, "./test", mcTex.name + "_" + j);
                        Object.Destroy(test);
                    }

                    mc.tempHistogram  = new Histogram(mc.isMask ? 4 : 3, config.histogramStepCount, 1f);
                    mc.histograms[j]  = histogram;
                    mc.HistPatches[j] = p;
                }

                if (isMonochromatic)
                {
                    unmatchedMcCount++;
                    unmatchedTexCount += mc.fileInfoIndices.Length;
                    reportSb.AppendLine(string.Concat(mcTex.name,
                                                      " is too consistent visually to be analyzed properly. Match it manually."));
                }
                else
                {
                    if (mc.bgPartIndex == -1)
                    {
                        mc.bgPartIndex = 0;
                    }

                    //Finally add the candidate to the list
                    candidatesList.Add(mc);
                }


                Object.Destroy(mcTex);
            }

            progressCb(new ProgressInfo("Preparing candidates", mcTexCount, mcTexCount, 1f));
            yield return(new WaitForEndOfFrame());

            //2. The match candidates are now all generated, we can go through all the BgInfo and for each BgInfo, going through all the MCs to find the best match.
            //For sure this way increase risk of false positives but it is also much faster than loading entire textures exponentially...
            var candidates   = candidatesList.ToArray();
            var baseDumpPath = Path.Combine(dumpTexPath, baseDumpFormat.name);
            //int mcMatchesCount = 0;
            var texMatchesCount = 0;

            for (var i = 0; i < bgInfoCount; i++)
            {
                //if (i % 10 == 0)
                //{
                progressCb(new ProgressInfo("Looking for matches", i + 1, bgInfoCount, i / (float)(bgInfoCount - 1)));
                yield return(new WaitForEndOfFrame());

                //}

                if (config.resetDumpMatches)
                {
                    bgInfos[i].ResetDumpMatches(dumpFormat.name);
                }

                //Every candidate is matched, stop searching.
                //TODO - Track the best BgInfo into the MCandidate. This could be perfect. But requires to get rid of that and the isMatched flag stored into the MC.
                //if (mcMatchesCount >= candidates.Length)
                //    break;

                var bgTex   = fm.GetTextureFromPath(Path.Combine(baseDumpPath, bgInfos[i].texDumpMatches[0].texNames[0]));
                var maskTex = bgInfos[i].hasMask
                    ? fm.GetTextureFromPath(Path.Combine(baseDumpPath, bgInfos[i].texDumpMatches[0].texNames[1]))
                    : null;

                //TODO - max possible match for one bg info and one bg part, List suck ass and realistically I never saw 3 textures exactly the same (and there is no duplicate on GC)
                var bestMatchValues =
                    new List <float> [dumpFormat.bgParts.Length == 0 ? 2: dumpFormat.bgParts.Length + 1];
                var bestMatchCandidateIndices =
                    new List <int> [dumpFormat.bgParts.Length == 0 ? 2: dumpFormat.bgParts.Length + 1];

                for (var j = 0; j < bestMatchValues.Length; j++)
                {
                    bestMatchValues[j]           = new List <float>();
                    bestMatchCandidateIndices[j] = new List <int>();
                }

                for (var j = 0; j < candidates.Length; j++)
                {
                    //if (candidates[j].isMatched)
                    //    continue;

                    var mc = candidates[j];

                    //Quick Pruning for mask, compare the size. Masks tend to have different size.
                    if (mc.isMask)
                    {
                        if (maskTex == null)
                        {
                            continue;
                        }

                        //In RE3, the masks have a fixed resolution on GC and a variable on PC (cropped)
                        if (config.inconsistentMaskSize)
                        {
                            if (maskTex.width < 128 || maskTex.height < 23)
                            {
                                continue;
                            }
                        }
                        else
                        {
                            if (maskTex.width != mc.texSize.x || maskTex.height != mc.texSize.y)
                            {
                                continue;
                            }
                        }
                    }
                    else
                    {
                        //This BG texture size comparison was certainly added for re3 but why?
                        //It's actually horrible for RE2 and 1. Thus I added a BG parts length check.
                        if (dumpFormat.bgParts.Length <= 1 && (bgTex.width != mc.texSize.x || bgTex.height != mc.texSize.y))
                        {
                            continue;
                        }
                    }

                    var mcMatchValue = 0f;

                    var isImpossibleMatch = false;
                    for (var k = 0; k < mc.HistPatches.Length; k++)
                    {
                        var p = mc.HistPatches[k];

                        mc.tempHistogram.Reset();

                        Color[] pixels;
                        if (mc.isMask)
                        {
                            if (config.inconsistentMaskSize)
                            {
                                var dstPosY = maskTex.height - 23 + (p.dstPos.y - (256 - 23));
                                pixels = maskTex.GetPixels(p.dstPos.x, dstPosY, p.size.x, p.size.y);
                                mc.tempHistogram.AddValues(pixels, true);
                            }
                            else
                            {
                                pixels = maskTex.GetPixels(p.dstPos.x, p.dstPos.y, p.size.x, p.size.y);
                                mc.tempHistogram.AddValues(pixels, true);
                            }
                        }
                        else
                        {
                            pixels = bgTex.GetPixels(p.dstPos.x, p.dstPos.y, p.size.x, p.size.y);
                            mc.tempHistogram.AddValues(pixels);
                        }

                        if (config.savePatchTexures)
                        {
                            var test = new Texture2D(p.size.x, p.size.y);
                            test.SetPixels(pixels);
                            fm.SaveTextureToPng(test, "./test", (mc.isMask ? maskTex.name : bgTex.name) + "_" + k);
                            Object.Destroy(test);
                        }

                        var patchMatchValue = mc.histograms[k].Compare(mc.tempHistogram);

                        //if the match value of one patch is SO BAD, you can stop here and move to the next MC.
                        if (patchMatchValue <= config.patchMinMatchValue)
                        {
                            isImpossibleMatch = true;
                            break;
                        }

                        mcMatchValue += patchMatchValue;
                    }

                    if (isImpossibleMatch)
                    {
                        continue;
                    }

                    mcMatchValue = mcMatchValue / mc.HistPatches.Length;

                    //bool isFirstValue = bestMatchValues[mc.bgPartIndex].Count <= 0;
                    if (mcMatchValue >= config.candidateMinMatchValue
                        ) // && (isFirstValue || mcMatchValue >= bestMatchValues[mc.bgPartIndex][0]))
                    {
                        //To deal with multiple perfect match... but I haven't already identified duplicates via checksum?
                        //if (!isFirstValue && mcMatchValue != bestMatchValues[mc.bgPartIndex][0])
                        //{
                        //    bestMatchValues[mc.bgPartIndex].Clear();
                        //}

                        bestMatchValues[mc.bgPartIndex].Add(mcMatchValue);
                        bestMatchCandidateIndices[mc.bgPartIndex].Add(j);
                    }
                }

                //Finally we store all the best matches found for that BG info
                //Again going through the bg info (and then the candidates) produces more false positives.
                //But the opposite is crazy bad in term of performance. An exponential amount of disk fetching.

                //For each part (typically 0 = Left, 1 = right, 2 mask)
                var partCount = bestMatchValues.Length;
                //bool hasMatches = false;
                for (var j = 0; j < partCount; j++)
                {
                    var matchCount = bestMatchValues[j].Count;
                    for (var k = 0; k < matchCount; k++)
                    {
                        //it should be defaulted to 0 anyway.
                        if (bestMatchValues[j][k] >= config.candidateMinMatchValue)
                        {
                            //candidates[bestMatchCandidateIndices[j][k]].isMatched = true;
                            var mc = candidates[bestMatchCandidateIndices[j][k]];
                            //for (int l = 0; l < mc.fileInfoIndices.Length; l++)
                            //{
                            //    string texName = fm.RemoveExtensionFromFileInfo(fm.fileInfos[mc.fileInfoIndices[l]]);
                            //    bgInfos[i].AddDumpMatch(dumpFormat.name, texName, mc.bgPartIndex);
                            //    //hasMatches = true;
                            //    texMatchesCount++;
                            //}
                            ////mcMatchesCount++;

                            mc.bgInfoMatchValue.Add(bestMatchValues[j][k]);
                            mc.bgInfoMatchIndex.Add(i);
                        }
                    }
                }

                //if (hasMatches)
                //    fm.SaveToJson(bgInfos[i], bgInfoPath, bgInfos[i].GetFileName(), prettifyJsonOnSave);

                Object.Destroy(bgTex);
                Object.Destroy(maskTex);
            }

            //Analyze the Match candidates' possible bg info matches
            for (var i = 0; i < candidates.Length; i++)
            {
                var mc = candidates[i];
                if (mc.bgInfoMatchIndex.Count <= 0)
                {
                    //Report the unmatchable candidates
                    unmatchedMcCount++;
                    reportSb.Append(string.Concat("Candidate ", i, " can't find a match: "));
                    for (var j = 0; j < candidates[i].fileInfoIndices.Length; j++)
                    {
                        unmatchedTexCount++;
                        reportSb.Append(string.Concat("[", fm.fileInfos[candidates[i].fileInfoIndices[j]].Name, "]"));
                    }

                    reportSb.AppendLine();
                }
                else
                {
                    var bestBgInfoMatchValue = 0f;
                    var bestBgInfoIndex      = 0;
                    for (var j = 0; j < mc.bgInfoMatchIndex.Count; j++)
                    {
                        if (mc.bgInfoMatchValue[j] > bestBgInfoMatchValue)
                        {
                            bestBgInfoMatchValue = mc.bgInfoMatchValue[j];
                            bestBgInfoIndex      = mc.bgInfoMatchIndex[j];
                        }
                    }

                    //Update and save the best bg info match
                    for (var j = 0; j < mc.fileInfoIndices.Length; j++)
                    {
                        var texName = fm.RemoveExtensionFromFileInfo(fm.fileInfos[mc.fileInfoIndices[j]]);

                        bgInfos[bestBgInfoIndex].AddDumpMatch(dumpFormat.name, texName, mc.bgPartIndex);
                        texMatchesCount++;
                    }

                    reportSb.Append(string.Concat("Candidate ", i, " matched: "));
                    for (var j = 0; j < candidates[i].fileInfoIndices.Length; j++)
                    {
                        reportSb.Append(string.Concat("[", fm.fileInfos[candidates[i].fileInfoIndices[j]].Name, "]"));
                    }

                    reportSb.Append(string.Concat(" - ", bgInfos[bestBgInfoIndex].namePrefix));
                    reportSb.Append(string.Concat(" - ", bestBgInfoMatchValue.ToString("0.00")));
                    reportSb.AppendLine();

                    fm.SaveToJson(bgInfos[bestBgInfoIndex], bgInfoPath, bgInfos[bestBgInfoIndex].GetFileName(),
                                  prettifyJsonOnSave);
                }
            }

            reportSb.AppendLine(string.Format(
                                    "{0} matches for {1} textures ({2} duplicates). {3} unmatchable textures from {4} candidates.",
                                    texMatchesCount, mcTexCount, texDuplicatesCount, unmatchedTexCount, unmatchedMcCount));
            reportSb.AppendLine(string.Format("== Texture Matching done! ({0} seconds) ==",
                                              (Time.unscaledTime - taskTime).ToString("#.0")));

            fm.OpenFolder(bgInfoPath);

            doneCb();
        }