Example #1
0
        private void CheckModForTFCCompactability(DeploymentChecklistItem item)
        {
            // if (ModBeingDeployed.Game >= Mod.MEGame.ME2)
            //{
            bool hasError = false;

            item.HasError = false;
            item.ItemText = M3L.GetString(M3L.string_checkingTexturesInMod);
            var        referencedFiles  = ModBeingDeployed.GetAllRelativeReferences().Select(x => Path.Combine(ModBeingDeployed.ModPath, x)).ToList();
            int        numChecked       = 0;
            GameTarget validationTarget = mainWindow.InstallationTargets.FirstOrDefault(x => x.Game == ModBeingDeployed.Game);
            var        errors           = new List <string>();

            foreach (var f in referencedFiles)
            {
                if (_closed)
                {
                    return;
                }
                numChecked++;
                item.ItemText = $@"{M3L.GetString(M3L.string_checkingTexturesInMod)} [{numChecked}/{referencedFiles.Count}]";
                if (f.RepresentsPackageFilePath())
                {
                    Log.Information(@"Checking file for broken textures: " + f);
                    var package  = MEPackageHandler.OpenMEPackage(f);
                    var textures = package.Exports.Where(x => x.IsTexture()).ToList();
                    foreach (var texture in textures)
                    {
                        if (_closed)
                        {
                            return;
                        }
                        var cache = texture.GetProperty <NameProperty>(@"TextureFileCacheName");
                        if (cache != null)
                        {
                            if (!VanillaDatabaseService.IsBasegameTFCName(cache.Value, ModBeingDeployed.Game))
                            {
                                var       mips = Texture2D.GetTexture2DMipInfos(texture, cache.Value);
                                Texture2D tex  = new Texture2D(texture);
                                try
                                {
                                    tex.GetImageBytesForMip(tex.GetTopMip(), validationTarget, false); //use active target
                                }
                                catch (Exception e)
                                {
                                    Log.Information(@"Found broken texture: " + texture.GetInstancedFullPath);
                                    hasError        = true;
                                    item.Icon       = FontAwesomeIcon.TimesCircle;
                                    item.Foreground = Brushes.Red;
                                    item.Spinning   = false;
                                    errors.Add(M3L.GetString(M3L.string_interp_couldNotLoadTextureData, texture.FileRef.FilePath, texture.GetInstancedFullPath, e.Message));
                                }
                            }
                        }
                    }
                }
            }

            if (!hasError)
            {
                item.Foreground = Brushes.Green;
                item.Icon       = FontAwesomeIcon.CheckCircle;
                item.ItemText   = M3L.GetString(M3L.string_noBrokenTexturesWereFound);
                item.ToolTip    = M3L.GetString(M3L.string_validationOK);
            }
            else
            {
                item.Errors   = errors;
                item.ItemText = M3L.GetString(M3L.string_textureIssuesWereDetected);
                item.ToolTip  = M3L.GetString(M3L.string_validationFailed);
            }
            item.HasError = hasError;
        }
Example #2
0
        private void Deployment_BackgroundThread(object sender, DoWorkEventArgs e)
        {
            var referencedFiles = ModBeingDeployed.GetAllRelativeReferences();

            string archivePath = e.Argument as string;
            //Key is in-archive path, value is on disk path
            var archiveMapping             = new Dictionary <string, string>();
            SortedSet <string> directories = new SortedSet <string>();

            foreach (var file in referencedFiles)
            {
                var path      = Path.Combine(ModBeingDeployed.ModPath, file);
                var directory = Directory.GetParent(path).FullName;
                if (directory.Length <= ModBeingDeployed.ModPath.Length)
                {
                    continue;                                                      //root file or directory.
                }
                directory = directory.Substring(ModBeingDeployed.ModPath.Length + 1);

                //nested folders with no folders
                var    relativeFolders    = directory.Split('\\');
                string buildingFolderList = "";
                foreach (var relativeFolder in relativeFolders)
                {
                    if (buildingFolderList != "")
                    {
                        buildingFolderList += @"\\";
                    }
                    buildingFolderList += relativeFolder;
                    if (directories.Add(buildingFolderList))
                    {
                        archiveMapping[buildingFolderList] = null;
                    }
                }
            }

            archiveMapping.AddRange(referencedFiles.ToDictionary(x => x, x => Path.Combine(ModBeingDeployed.ModPath, x)));

            var compressor = new SevenZip.SevenZipCompressor();

            //compressor.CompressionLevel = CompressionLevel.Ultra;
            compressor.CustomParameters.Add(@"s", @"on");
            if (!MultithreadedCompression)
            {
                compressor.CustomParameters.Add(@"mt", @"off");
            }
            compressor.CustomParameters.Add(@"yx", @"9");
            //compressor.CustomParameters.Add("x", "9");
            compressor.CustomParameters.Add(@"d", @"28");
            string currentDeploymentStep = M3L.GetString(M3L.string_mod);

            compressor.Progressing += (a, b) =>
            {
                //Debug.WriteLine(b.AmountCompleted + "/" + b.TotalAmount);
                ProgressMax   = b.TotalAmount;
                ProgressValue = b.AmountCompleted;
                var now = DateTime.Now;
                if ((now - lastPercentUpdateTime).Milliseconds > ModInstaller.PERCENT_REFRESH_COOLDOWN)
                {
                    //Don't update UI too often. Once per second is enough.
                    string percent = (ProgressValue * 100.0 / ProgressMax).ToString(@"0.00");
                    OperationText         = $@"[{currentDeploymentStep}] {M3L.GetString(M3L.string_deploymentInProgress)} {percent}%";
                    lastPercentUpdateTime = now;
                }
                //Debug.WriteLine(ProgressValue + "/" + ProgressMax);
            };
            compressor.FileCompressionStarted += (a, b) => { Debug.WriteLine(b.FileName); };
            compressor.CompressFileDictionary(archiveMapping, archivePath);
            compressor.CustomParameters.Clear(); //remove custom params as it seems to force LZMA
            compressor.CompressionMode  = CompressionMode.Append;
            compressor.CompressionLevel = CompressionLevel.None;
            currentDeploymentStep       = @"moddesc.ini";
            compressor.CompressFiles(archivePath, new string[]
            {
                Path.Combine(ModBeingDeployed.ModPath, @"moddesc.ini")
            });
            OperationText = M3L.GetString(M3L.string_deploymentSucceeded);
            Utilities.HighlightInExplorer(archivePath);
        }
Example #3
0
        private void CheckModForAFCCompactability(DeploymentChecklistItem item)
        {
            bool hasError = false;

            item.HasError = false;
            item.ItemText = M3L.GetString(M3L.string_checkingAudioReferencesInMod);
            var           referencedFiles  = ModBeingDeployed.GetAllRelativeReferences().Select(x => Path.Combine(ModBeingDeployed.ModPath, x)).ToList();
            int           numChecked       = 0;
            GameTarget    validationTarget = mainWindow.InstallationTargets.FirstOrDefault(x => x.Game == ModBeingDeployed.Game);
            List <string> gameFiles        = MEDirectories.EnumerateGameFiles(validationTarget.Game, validationTarget.TargetPath);

            var errors = new List <string>();
            Dictionary <string, MemoryStream> cachedAudio = new Dictionary <string, MemoryStream>();

            foreach (var f in referencedFiles)
            {
                if (_closed)
                {
                    return;
                }
                numChecked++;
                item.ItemText = $@"{M3L.GetString(M3L.string_checkingAudioReferencesInMod)} [{numChecked}/{referencedFiles.Count}]";
                if (f.RepresentsPackageFilePath())
                {
                    var package      = MEPackageHandler.OpenMEPackage(f);
                    var wwiseStreams = package.Exports.Where(x => x.ClassName == @"WwiseStream" && !x.IsDefaultObject).ToList();
                    foreach (var wwisestream in wwiseStreams)
                    {
                        if (_closed)
                        {
                            return;
                        }
                        //Check each reference.
                        var afcNameProp = wwisestream.GetProperty <NameProperty>(@"Filename");
                        if (afcNameProp != null)
                        {
                            string afcNameWithExtension = afcNameProp + @".afc";
                            int    audioSize            = BitConverter.ToInt32(wwisestream.Data, wwisestream.Data.Length - 8);
                            int    audioOffset          = BitConverter.ToInt32(wwisestream.Data, wwisestream.Data.Length - 4);

                            string afcPath               = null;
                            Stream audioStream           = null;
                            var    localDirectoryAFCPath = Path.Combine(Path.GetDirectoryName(wwisestream.FileRef.FilePath), afcNameWithExtension);
                            bool   isInOfficialArea      = false;
                            if (File.Exists(localDirectoryAFCPath))
                            {
                                //local afc
                                afcPath = localDirectoryAFCPath;
                            }
                            else
                            {
                                //Check game
                                var fullPath = gameFiles.FirstOrDefault(x => Path.GetFileName(x).Equals(afcNameWithExtension, StringComparison.InvariantCultureIgnoreCase));
                                if (fullPath != null)
                                {
                                    afcPath          = fullPath;
                                    isInOfficialArea = MEDirectories.IsInBasegame(afcPath, validationTarget) || MEDirectories.IsInOfficialDLC(afcPath, validationTarget);
                                }
                                else if (cachedAudio.TryGetValue(afcNameProp.Value.Name, out var cachedAudioStream))
                                {
                                    audioStream = cachedAudioStream;
                                    //isInOfficialArea = true; //cached from vanilla SFAR
                                }
                                else if (MEDirectories.OfficialDLC(validationTarget.Game).Any(x => afcNameProp.Value.Name.StartsWith(x)))
                                {
                                    var dlcName = afcNameProp.Value.Name.Substring(0, afcNameProp.Value.Name.LastIndexOf(@"_", StringComparison.InvariantCultureIgnoreCase));
                                    var audio   = VanillaDatabaseService.FetchFileFromVanillaSFAR(validationTarget, dlcName, afcNameWithExtension);
                                    if (audio != null)
                                    {
                                        cachedAudio[afcNameProp.Value.Name] = audio;
                                    }

                                    audioStream = audio;
                                    //isInOfficialArea = true; as this is in a vanilla SFAR we don't test against this since it will be correct.
                                    continue;
                                }
                                else
                                {
                                    hasError        = true;
                                    item.Icon       = FontAwesomeIcon.TimesCircle;
                                    item.Foreground = Brushes.Red;
                                    item.Spinning   = false;
                                    errors.Add(M3L.GetString(M3L.string_interp_couldNotFindReferencedAFC, wwisestream.FileRef.FilePath, wwisestream.GetInstancedFullPath, afcNameProp.ToString()));
                                    continue;
                                }
                            }

                            if (afcPath != null)
                            {
                                audioStream = new FileStream(afcPath, FileMode.Open);
                            }

                            try
                            {
                                audioStream.Seek(audioOffset, SeekOrigin.Begin);
                                if (audioStream.ReadStringASCIINull(4) != @"RIFF")
                                {
                                    hasError        = true;
                                    item.Icon       = FontAwesomeIcon.TimesCircle;
                                    item.Foreground = Brushes.Red;
                                    item.Spinning   = false;
                                    errors.Add(M3L.GetString(M3L.string_interp_invalidAudioPointer, wwisestream.FileRef.FilePath, wwisestream.GetInstancedFullPath));
                                    if (audioStream is FileStream)
                                    {
                                        audioStream.Close();
                                    }
                                    continue;
                                }

                                //attempt to seek audio length.
                                audioStream.Seek(audioSize + 4, SeekOrigin.Current);

                                //Check if this file is in basegame
                                if (isInOfficialArea)
                                {
                                    //Verify offset is not greater than vanilla size
                                    var vanillaInfo = VanillaDatabaseService.GetVanillaFileInfo(validationTarget, afcPath.Substring(validationTarget.TargetPath.Length + 1));
                                    if (vanillaInfo == null)
                                    {
                                        Crashes.TrackError(new Exception($@"Vanilla information was null when performing vanilla file check for {afcPath.Substring(validationTarget.TargetPath.Length + 1)}"));
                                    }
                                    if (audioOffset >= vanillaInfo[0].size)
                                    {
                                        hasError        = true;
                                        item.Icon       = FontAwesomeIcon.TimesCircle;
                                        item.Foreground = Brushes.Red;
                                        item.Spinning   = false;
                                        errors.Add(M3L.GetString(M3L.string_interp_audioStoredInOfficialAFC, wwisestream.FileRef.FilePath, wwisestream.GetInstancedFullPath));
                                    }
                                }
                                if (audioStream is FileStream)
                                {
                                    audioStream.Close();
                                }
                            }
                            catch (Exception e)
                            {
                                hasError        = true;
                                item.Icon       = FontAwesomeIcon.TimesCircle;
                                item.Foreground = Brushes.Red;
                                item.Spinning   = false;
                                if (audioStream is FileStream)
                                {
                                    audioStream.Close();
                                }
                                errors.Add(M3L.GetString(M3L.string_errorValidatingAudioReference, wwisestream.FileRef.FilePath, wwisestream.GetInstancedFullPath, e.Message));
                                continue;
                            }
                        }
                    }
                }
            }


            if (!hasError)
            {
                item.Foreground = Brushes.Green;
                item.Icon       = FontAwesomeIcon.CheckCircle;
                item.ItemText   = M3L.GetString(M3L.string_noAudioIssuesWereDetected);
                item.ToolTip    = M3L.GetString(M3L.string_validationOK);
            }
            else
            {
                item.Errors   = errors;
                item.ItemText = M3L.GetString(M3L.string_audioIssuesWereDetected);
                item.ToolTip  = M3L.GetString(M3L.string_validationFailed);
            }
            item.HasError = hasError;
            cachedAudio.Clear();
        }
Example #4
0
        private void CheckModSFARs(DeploymentChecklistItem item)
        {
            bool hasError = false;

            item.HasError = false;
            item.ItemText = M3L.GetString(M3L.string_checkingSFARFilesSizes);
            var           referencedFiles  = ModBeingDeployed.GetAllRelativeReferences().Select(x => Path.Combine(ModBeingDeployed.ModPath, x)).ToList();
            int           numChecked       = 0;
            GameTarget    validationTarget = mainWindow.InstallationTargets.FirstOrDefault(x => x.Game == ModBeingDeployed.Game);
            List <string> gameFiles        = MEDirectories.EnumerateGameFiles(validationTarget.Game, validationTarget.TargetPath);

            var errors = new List <string>();
            Dictionary <string, MemoryStream> cachedAudio = new Dictionary <string, MemoryStream>();

            bool hasSFARs = false;

            foreach (var f in referencedFiles)
            {
                if (_closed)
                {
                    return;
                }
                if (Path.GetExtension(f) == @".sfar")
                {
                    hasSFARs = true;
                    if (new FileInfo(f).Length != 32)
                    {
                        {
                            hasError        = true;
                            item.Icon       = FontAwesomeIcon.TimesCircle;
                            item.Foreground = Brushes.Red;
                            item.Spinning   = false;
                            errors.Add(f);
                        }
                    }
                }
            }

            if (!hasSFARs)
            {
                item.Foreground = Brushes.Green;
                item.Icon       = FontAwesomeIcon.CheckCircle;
                item.ItemText   = M3L.GetString(M3L.string_modDoesNotUseSFARs);
                item.ToolTip    = M3L.GetString(M3L.string_validationOK);
            }
            else
            {
                if (!hasError)
                {
                    item.Foreground = Brushes.Green;
                    item.Icon       = FontAwesomeIcon.CheckCircle;
                    item.ItemText   = M3L.GetString(M3L.string_noSFARSizeIssuesWereDetected);
                    item.ToolTip    = M3L.GetString(M3L.string_validationOK);
                }
                else
                {
                    item.Errors   = errors;
                    item.ItemText = M3L.GetString(M3L.string_someSFARSizesAreTheIncorrectSize);
                    item.ToolTip  = M3L.GetString(M3L.string_validationFailed);
                }

                item.HasError = hasError;
            }
        }
Example #5
0
        private void CheckLocalizationsME3(DeploymentChecklistItem obj)
        {
            var customDLCJob     = ModBeingDeployed.GetJob(ModJob.JobHeader.CUSTOMDLC);
            var customDLCFolders = customDLCJob.CustomDLCFolderMapping.Keys.ToList();

            customDLCFolders.AddRange(customDLCJob.AlternateDLCs.Where(x => x.Operation == AlternateDLC.AltDLCOperation.OP_ADD_CUSTOMDLC).Select(x => x.AlternateDLCFolder));
            var           languages = StarterKitGeneratorWindow.me3languages;
            List <string> errors    = new List <string>();

            obj.ItemText = M3L.GetString(M3L.string_languageCheckInProgress);
            foreach (var customDLC in customDLCFolders)
            {
                if (_closed)
                {
                    return;
                }
                var tlkBasePath = Path.Combine(ModBeingDeployed.ModPath, customDLC, @"CookedPCConsole", customDLC);
                Dictionary <string, List <TalkFileME1.TLKStringRef> > tlkMappings = new Dictionary <string, List <TalkFileME1.TLKStringRef> >();
                foreach (var language in languages)
                {
                    if (_closed)
                    {
                        return;
                    }
                    var tlkLangPath = tlkBasePath + @"_" + language.filecode + @".tlk";
                    if (File.Exists(tlkLangPath))
                    {
                        //inspect
                        TalkFileME2ME3 tf = new TalkFileME2ME3();
                        tf.LoadTlkData(tlkLangPath);
                        tlkMappings[language.filecode] = tf.StringRefs;
                    }
                    else
                    {
                        errors.Add(M3L.GetString(M3L.string_interp_customDLCMissingLocalizedTLK, customDLC, language.filecode));;
                    }
                }
                if (tlkMappings.Count > 1)
                {
                    //find TLK with most entries
                    //var tlkCounts = tlkMappings.Select(x => (x.Key, x.Value.Count));
                    double numLoops = Math.Pow(tlkMappings.Count - 1, tlkMappings.Count - 1);
                    int    numDone  = 0;
                    foreach (var mapping1 in tlkMappings)
                    {
                        foreach (var mapping2 in tlkMappings)
                        {
                            if (mapping1.Equals(mapping2))
                            {
                                continue;
                            }

                            var differences = mapping1.Value.Select(x => x.StringID).Except(mapping2.Value.Select(x => x.StringID));
                            foreach (var difference in differences)
                            {
                                var str = mapping1.Value.FirstOrDefault(x => x.StringID == difference)?.Data ?? M3L.GetString(M3L.string_errorFindingString);
                                errors.Add(M3L.GetString(M3L.string_interp_tlkDifference, difference.ToString(), mapping1.Key, mapping2.Key, str));
                            }

                            numDone++;
                            double percent = (numDone * 100.0) / numLoops;
                            obj.ItemText = $@"{M3L.GetString(M3L.string_languageCheckInProgress)} {percent:0.00}%";
                        }
                    }
                    //use INT as master. Not sure if any mods are not-english based
                    //TODO
                }
            }
            if (errors.Count > 0)
            {
                obj.HasError   = true;
                obj.Icon       = FontAwesomeIcon.Warning;
                obj.Foreground = Brushes.Orange;
                obj.Errors     = errors;
                obj.ItemText   = M3L.GetString(M3L.string_languageCheckDetectedIssues);
                obj.ToolTip    = M3L.GetString(M3L.string_validationFailed);
            }
            else
            {
                obj.Icon       = FontAwesomeIcon.CheckCircle;
                obj.Foreground = Brushes.Green;
                obj.ItemText   = M3L.GetString(M3L.string_noLanguageIssuesDetected);
                obj.ToolTip    = M3L.GetString(M3L.string_validationOK);
            }
        }
        private void CheckModForTFCCompactability(DeploymentChecklistItem item)
        {
            // if (ModBeingDeployed.Game >= Mod.MEGame.ME2)
            //{
            bool hasError = false;

            item.HasError = false;
            item.ItemText = "Checking textures in mod";
            var        referencedFiles  = ModBeingDeployed.GetAllRelativeReferences().Select(x => Path.Combine(ModBeingDeployed.ModPath, x)).ToList();
            int        numChecked       = 0;
            GameTarget validationTarget = mainWindow.InstallationTargets.FirstOrDefault(x => x.Game == ModBeingDeployed.Game);
            var        errors           = new List <string>();

            foreach (var f in referencedFiles)
            {
                if (_closed)
                {
                    return;
                }
                numChecked++;
                item.ItemText = $"Checking textures in mod [{numChecked}/{referencedFiles.Count}]";
                if (f.RepresentsPackageFilePath())
                {
                    var package  = MEPackageHandler.OpenMEPackage(f);
                    var textures = package.Exports.Where(x => x.IsTexture()).ToList();
                    foreach (var texture in textures)
                    {
                        if (_closed)
                        {
                            return;
                        }
                        var cache = texture.GetProperty <NameProperty>("TextureFileCacheName");
                        if (cache != null)
                        {
                            if (!VanillaDatabaseService.IsBasegameTFCName(cache.Value, ModBeingDeployed.Game))
                            {
                                var       mips = Texture2D.GetTexture2DMipInfos(texture, cache.Value);
                                Texture2D tex  = new Texture2D(texture);
                                try
                                {
                                    tex.GetImageBytesForMip(tex.GetTopMip(), validationTarget, false); //use active target
                                }
                                catch (Exception e)
                                {
                                    hasError        = true;
                                    item.Icon       = FontAwesomeIcon.TimesCircle;
                                    item.Foreground = Brushes.Red;
                                    item.Spinning   = false;
                                    errors.Add($"{texture.FileRef.FilePath} - {texture.GetInstancedFullPath}: Could not load texture data: {e.Message}");
                                }
                            }
                        }
                    }
                }
            }

            if (!hasError)
            {
                item.Foreground = Brushes.Green;
                item.Icon       = FontAwesomeIcon.CheckCircle;
                item.ItemText   = "No broken textures were found";
                item.ToolTip    = "Validation passed";
            }
            else
            {
                item.Errors   = errors;
                item.ItemText = "Texture issues were detected";
                item.ToolTip  = "Validation failed";
            }
            item.HasError = hasError;
            //}
            //} else
            //{
            //    item.Foreground = Brushes.Green;
            //    item.Icon = FontAwesomeIcon.CheckCircle;
            //    item.ItemText = "Textures are not ";
            //    item.ToolTip = "Validation passed";
            //}
        }
        private void CheckLocalizationsME3(DeploymentChecklistItem obj)
        {
            var customDLCJob     = ModBeingDeployed.GetJob(ModJob.JobHeader.CUSTOMDLC);
            var customDLCFolders = customDLCJob.CustomDLCFolderMapping.Keys.ToList();

            customDLCFolders.AddRange(customDLCJob.AlternateDLCs.Where(x => x.Operation == AlternateDLC.AltDLCOperation.OP_ADD_CUSTOMDLC).Select(x => x.AlternateDLCFolder));
            var           languages = StarterKitGeneratorWindow.me3languages;
            List <string> errors    = new List <string>();

            obj.ItemText = "Language check in progress";
            foreach (var customDLC in customDLCFolders)
            {
                if (_closed)
                {
                    return;
                }
                var tlkBasePath = Path.Combine(ModBeingDeployed.ModPath, customDLC, "CookedPCConsole", customDLC);
                Dictionary <string, List <TalkFileME1.TLKStringRef> > tlkMappings = new Dictionary <string, List <TalkFileME1.TLKStringRef> >();
                foreach (var language in languages)
                {
                    if (_closed)
                    {
                        return;
                    }
                    var tlkLangPath = tlkBasePath + "_" + language.filecode + ".tlk";
                    if (File.Exists(tlkLangPath))
                    {
                        //inspect
                        TalkFileME2ME3 tf = new TalkFileME2ME3();
                        tf.LoadTlkData(tlkLangPath);
                        tlkMappings[language.filecode] = tf.StringRefs;
                    }
                    else
                    {
                        errors.Add(customDLC + " is missing a localized TLK for language " + language.filecode + ". This DLC will not load if the user's game language is set to this. Some versions of the game cannot have their language changed, so this will effectively lock the user out from using this mod.");
                    }
                }
                if (tlkMappings.Count > 1)
                {
                    //find TLK with most entries
                    //var tlkCounts = tlkMappings.Select(x => (x.Key, x.Value.Count));
                    double numLoops = Math.Pow(tlkMappings.Count - 1, tlkMappings.Count - 1);
                    int    numDone  = 0;
                    foreach (var mapping1 in tlkMappings)
                    {
                        foreach (var mapping2 in tlkMappings)
                        {
                            if (mapping1.Equals(mapping2))
                            {
                                continue;
                            }

                            var differences = mapping1.Value.Select(x => x.StringID).Except(mapping2.Value.Select(x => x.StringID));
                            foreach (var difference in differences)
                            {
                                var str = mapping1.Value.FirstOrDefault(x => x.StringID == difference)?.Data ?? "<error finding string>";
                                errors.Add($"TLKStringID {difference} is present in {mapping1.Key}  but is not present in {mapping2.Key}. Even if this mod is not truly localized to another language, the strings should be copied into other language TLK files to ensure users of that language will see strings instead of string references.\n{str}");
                            }

                            numDone++;
                            double percent = (numDone * 100.0) / numLoops;
                            obj.ItemText = $"Language check in progress {percent:0.00}%";
                        }
                    }
                    //use INT as master. Not sure if any mods are not-english based
                    //TODO
                }
            }
            if (errors.Count > 0)
            {
                obj.HasError   = true;
                obj.Icon       = FontAwesomeIcon.Warning;
                obj.Foreground = Brushes.Orange;
                obj.Errors     = errors;
                obj.ItemText   = "Language check detected issues";
                obj.ToolTip    = "Validation failed";
            }
            else
            {
                obj.Icon       = FontAwesomeIcon.CheckCircle;
                obj.Foreground = Brushes.Green;
                obj.ItemText   = "No language issues detected";
                obj.ToolTip    = "Validation OK";
            }
        }