/// <summary> /// Serializes this object to it's moddesc.ini representation /// </summary> /// <returns></returns> public string Serialize() { var props = new Dictionary <string, string>(); props[@"Condition"] = Condition.ToString(); //always set props[@"ConditionalDLC"] = string.Join(';', ConditionalDLC); props[@"ModOperation"] = Operation.ToString(); //always set props[@"ModAltDLC"] = AlternateDLCFolder; props[@"ModDestDLC"] = DestinationDLCFolder; props[@"FriendlyName"] = FriendlyName; props[@"Description"] = Description; if (CheckedByDefault) { props[@"CheckedByDefault"] = CheckedByDefault.ToString(); } if (!string.IsNullOrWhiteSpace(GroupName)) { props[@"OptionGroup"] = GroupName; } if (!string.IsNullOrWhiteSpace(ApplicableAutoText)) { props[@"ApplicableAutoText"] = ApplicableAutoText; } if (!string.IsNullOrWhiteSpace(NotApplicableAutoText)) { props[@"NotApplicableAutoText"] = NotApplicableAutoText; } if (!string.IsNullOrWhiteSpace(MultiListRootPath)) { props[@"MultiListRootPath"] = MultiListRootPath; } // TODO: MULTILISTID... tied to job somehow. if (RequiredSpecificFiles.Any()) { var paths = ""; var sizes = ""; foreach (var v in RequiredSpecificFiles) { if (paths != @"") { paths += @";"; } if (sizes != @"") { sizes += @";"; } paths += v.Key; // should we check for spaces? Can game files support spaces? sizes += v.Value; } props[@"RequiredFileRelativePaths"] = paths; props[@"RequiredFileSizes"] = sizes; } return(StringStructParser.BuildCommaSeparatedSplitValueList(props)); }
public MetaCMM(string metaFile) { var lines = File.ReadAllLines(metaFile); int i = 0; foreach (var line in lines) { switch (i) { case 0: ModName = line; break; case 1: Version = line; break; case 2: InstalledBy = line; break; case 3: InstallerInstanceGUID = line; break; default: // MetaCMM Extended if (line.StartsWith(PrefixOptionsSelectedOnInstall)) { var parsedline = line.Substring(PrefixOptionsSelectedOnInstall.Length); OptionsSelectedAtInstallTime.ReplaceAll(StringStructParser.GetSemicolonSplitList(parsedline)); } else if (line.StartsWith(PrefixIncompatibleDLC)) { var parsedline = line.Substring(PrefixIncompatibleDLC.Length); IncompatibleDLC.ReplaceAll(StringStructParser.GetSemicolonSplitList(parsedline)); } break; } i++; } }
public static string ConvertAlternatesToALOTManifestMod(string inputText) { var conditionalStructs = StringStructParser.GetParenthesisSplitValues(inputText); var redirects = new List <extractionredirect>(); XElement root = new XElement("root"); foreach (var cstruct in conditionalStructs) { XElement element = new XElement("extractionredirect"); root.Add(element); var parsed = StringStructParser.GetCommaSplitValues(cstruct); var redirect = new extractionredirect(); if (parsed.TryGetValue("ModAltDLC", out var arp)) { element.SetAttributeValue("archiverootpath", arp.Replace('/', '\\')); } if (parsed.TryGetValue("ModDestDLC", out var rdd)) { element.SetAttributeValue("relativedestinationdirectory", @"BIOGame\DLC\" + rdd.Replace('/', '\\')); } if (parsed.TryGetValue("ConditionalDLC", out var ord)) { element.SetAttributeValue("optionalrequireddlc", string.Join(';', StringStructParser.GetSemicolonSplitList(ord))); } if (parsed.TryGetValue("RequiredFileRelativePaths", out var orf)) { element.SetAttributeValue("optionalrequiredfiles", string.Join(';', StringStructParser.GetSemicolonSplitList(orf).Select(x => x.Replace('/', '\\')))); } if (parsed.TryGetValue("RequiredFileSizes", out var orfs)) { element.SetAttributeValue("optionalrequiredfilessizes", string.Join(';', StringStructParser.GetSemicolonSplitList(orfs).Select(x => long.Parse(x)))); } if (parsed.TryGetValue("FriendlyName", out var ln)) { element.SetAttributeValue("loggingname", ln); } redirects.Add(redirect); //just for later convenience if i refactor this } return(root.ToString()); }
public override void Serialize(IniData ini) { if (CustomDLCJob != null && Alternates.Any()) { string outStr = "("; bool isFirst = true; foreach (var adlc in Alternates) { if (isFirst) { isFirst = false; } else { outStr += @","; } outStr += StringStructParser.BuildCommaSeparatedSplitValueList(adlc.ParameterMap.Where(x => !string.IsNullOrWhiteSpace(x.Value)).ToDictionary(x => x.Key, x => x.Value)); } outStr += ")"; ini[@"CUSTOMDLC"][@"altdlc"] = outStr; } }
public MetaCMM(string metaFile) { var lines = Utilities.WriteSafeReadAllLines(metaFile).ToList(); int i = 0; foreach (var line in lines) { switch (i) { case 0: ModName = line; break; case 1: Version = line; break; case 2: InstalledBy = line; break; case 3: InstallerInstanceGUID = line; break; default: if (line.StartsWith(ControllerCompatMetaPrefix)) { var parsedline = line.Substring(ControllerCompatMetaPrefix.Length); ME3ControllerModCompatBuiltAgainst.ReplaceAll(StringStructParser.GetSemicolonSplitList(parsedline)); } break; } i++; } }
public AlternateDLC(string alternateDLCText, mod.Mod modForValidating, ModJob job) { var properties = StringStructParser.GetCommaSplitValues(alternateDLCText); //todo: if statements to check these. if (properties.TryGetValue(@"FriendlyName", out string friendlyName)) { FriendlyName = friendlyName; } if (modForValidating.ModDescTargetVersion >= 6 && string.IsNullOrWhiteSpace(FriendlyName)) { //Cannot be null. Log.Error(@"Alternate DLC does not specify FriendlyName. Mods targeting moddesc >= 6.0 require FriendlyName"); ValidAlternate = false; LoadFailedReason = M3L.GetString(M3L.string_validation_altdlc_oneAltDlcMissingFriendlyNameCmm6); return; } if (!Enum.TryParse(properties[@"Condition"], out Condition)) { Log.Error($@"Alternate DLC specifies unknown/unsupported condition: {properties[@"Condition"]}"); //do not localize ValidAlternate = false; var condition = properties[@"Condition"]; LoadFailedReason = $@"{M3L.GetString(M3L.string_validation_altdlc_unknownCondition)} {condition}"; return; } if (!Enum.TryParse(properties[@"ModOperation"], out Operation)) { Log.Error($@"Alternate DLC specifies unknown/unsupported operation: {properties[@"ModOperation"]}"); //do not localize ValidAlternate = false; var operation = properties[@"ModOperation"]; LoadFailedReason = $@"{M3L.GetString(M3L.string_validation_altdlc_unknownOperation)} {operation}"; return; } if (properties.TryGetValue(@"Description", out string description)) { Description = description; } if (modForValidating.ModDescTargetVersion >= 6 && string.IsNullOrWhiteSpace(Description)) { //Cannot be null. Log.Error($@"Alternate DLC {FriendlyName} cannot have empty Description or missing Description descriptor as it targets cmmver >= 6"); ValidAlternate = false; LoadFailedReason = M3L.GetString(M3L.string_interp_validation_altdlc_cmmver6RequiresDescription, FriendlyName); return; } //OP_NOTHING can have conditions if (properties.TryGetValue(@"ConditionalDLC", out string conditionalDlc)) { var conditionalList = StringStructParser.GetSemicolonSplitList(conditionalDlc); foreach (var dlc in conditionalList) { if (Condition == AltDLCCondition.COND_MANUAL) { if (modForValidating.ModDescTargetVersion >= 6.3) { // On 6.3 trigger failure on this mod to help ensure users design mod properly Log.Error($@"{modForValidating.ModName} has Alternate DLC {friendlyName} that has a value for ConditionalDLC on Condition COND_MANUAL. COND_MANUAL does not use ConditionalDLC, use DLCRequirements instead."); ValidAlternate = false; LoadFailedReason = M3L.GetString(M3L.string_interp_validation_altdlc_condManualWithConditionalDLC, friendlyName); return; } else { Log.Warning($@"{modForValidating.ModName} has AlternateDLC {friendlyName} that has a value for ConditionalDLC on Condition COND_MANUAL. COND_MANUAL does not use ConditionalDLC, use DLCRequirements instead. On mods targetting moddesc 6.3 and above, this will trigger a load failure for a mod."); } break; } else if (Condition == AltDLCCondition.COND_SPECIFIC_DLC_SETUP) { //check +/- if (!dlc.StartsWith(@"-") && !dlc.StartsWith(@"+")) { Log.Error($@"An item in Alternate DLC's ({FriendlyName}) ConditionalDLC doesn't start with + or -. When using the condition {Condition}, you must precede DLC names with + or -. Bad value: {dlc}"); LoadFailedReason = M3L.GetString(M3L.string_interp_validation_altdlc_specificDlcSetupMissingPlusMinus, FriendlyName, Condition, dlc); return; } var prefix = dlc.Substring(0, 1); var realname = dlc.Substring(1); //official headers if (Enum.TryParse(realname, out ModJob.JobHeader header) && ModJob.GetHeadersToDLCNamesMap(modForValidating.Game).TryGetValue(header, out var foldername)) { ConditionalDLC.Add(prefix + foldername); continue; } //dlc mods if (!realname.StartsWith(@"DLC_")) { Log.Error($@"An item in Alternate DLC's ({FriendlyName}) ConditionalDLC doesn't start with DLC_ or is not official header (after the +/- required by {Condition}). Bad value: {dlc}"); LoadFailedReason = M3L.GetString(M3L.string_interp_validation_altdlc_specificDlcSetupInvalidDlcName, FriendlyName, Condition, dlc); return; } else { ConditionalDLC.Add(prefix + realname); } } else { if (Enum.TryParse(dlc, out ModJob.JobHeader header) && ModJob.GetHeadersToDLCNamesMap(modForValidating.Game).TryGetValue(header, out var foldername)) { ConditionalDLC.Add(foldername); continue; } if (!dlc.StartsWith(@"DLC_")) { Log.Error($@"An item in Alternate DLC's ({FriendlyName}) ConditionalDLC doesn't start with DLC_ or is not official header"); LoadFailedReason = M3L.GetString(M3L.string_interp_validation_altdlc_conditionalDLCInvalidValue, FriendlyName); return; } else { ConditionalDLC.Add(dlc); } } } } if (Operation != AltDLCOperation.OP_NOTHING) { int multilistid = -1; if (Operation == AltDLCOperation.OP_ADD_MULTILISTFILES_TO_CUSTOMDLC) { if (properties.TryGetValue(@"MultiListRootPath", out var rootpath)) { MultiListRootPath = rootpath.TrimStart('\\', '/').Replace('/', '\\'); } else { Log.Error($@"Alternate DLC ({FriendlyName}) specifies operation OP_ADD_MULTILISTFILES_TO_CUSTOMDLC but does not specify the required item MultiListRootPath."); ValidAlternate = false; LoadFailedReason = M3L.GetString(M3L.string_interp_altdlc_multilistMissingMultiListRootPath, FriendlyName); return; } if (properties.TryGetValue(@"MultiListId", out string multilistidstr) && int.TryParse(multilistidstr, out multilistid)) { if (job.MultiLists.TryGetValue(multilistid, out var ml)) { MultiListId = multilistid; MultiListSourceFiles = ml.Select(x => x.TrimStart('\\', '/')).ToArray(); } else { Log.Error($@"Alternate DLC ({FriendlyName}) Multilist ID does not exist as part of the {job.Header} task: multilist" + multilistid); ValidAlternate = false; var id = @"multilist" + multilistid; LoadFailedReason = M3L.GetString(M3L.string_interp_altdlc_multilistMissingMultiListX, FriendlyName, job.Header, id); return; } } else { Log.Error($@"Alternate DLC ({FriendlyName}) specifies operation OP_ADD_MULTILISTFILES_TO_CUSTOMDLC but does not specify the MultiListId attribute, or it could not be parsed to an integer."); ValidAlternate = false; LoadFailedReason = M3L.GetString(M3L.string_interp_altdlc_multilistIdNotIntegerOrMissing, FriendlyName); return; } } else { if (properties.TryGetValue(@"ModAltDLC", out string altDLCFolder)) { AlternateDLCFolder = altDLCFolder.Replace('/', '\\'); } else { Log.Error(@"Alternate DLC does not specify ModAltDLC but is required"); ValidAlternate = false; LoadFailedReason = M3L.GetString(M3L.string_interp_validation_altdlc_missingModAltDLC, FriendlyName); return; } } if (properties.TryGetValue(@"ModDestDLC", out string destDLCFolder)) { DestinationDLCFolder = destDLCFolder.Replace('/', '\\'); } else { Log.Error(@"Alternate DLC does not specify ModDestDLC but is required"); ValidAlternate = false; LoadFailedReason = M3L.GetString(M3L.string_interp_validation_altdlc_missingModDestDLC, FriendlyName); return; } //todo: Validate target in mod folder //Validation if (string.IsNullOrWhiteSpace(AlternateDLCFolder) && MultiListRootPath == null) { Log.Error($@"Alternate DLC directory (ModAltDLC) not specified for {FriendlyName}"); LoadFailedReason = M3L.GetString(M3L.string_interp_validation_altdlc_sourceDirectoryNotSpecifiedForModAltDLC, FriendlyName); return; } if (string.IsNullOrWhiteSpace(DestinationDLCFolder)) { Log.Error($@"Destination DLC directory (ModDestDLC) not specified for {FriendlyName}"); LoadFailedReason = M3L.GetString(M3L.string_interp_validation_altdlc_destinationDirectoryNotSpecifiedForModDestDLC, FriendlyName); return; } if (AlternateDLCFolder != null) { AlternateDLCFolder = AlternateDLCFolder.TrimStart('\\', '/').Replace('/', '\\'); //Check ModAltDLC directory exists var localAltDlcDir = FilesystemInterposer.PathCombine(modForValidating.IsInArchive, modForValidating.ModPath, AlternateDLCFolder); if (!FilesystemInterposer.DirectoryExists(localAltDlcDir, modForValidating.Archive)) { Log.Error($@"Alternate DLC directory (ModAltDLC) does not exist: {AlternateDLCFolder}"); LoadFailedReason = M3L.GetString(M3L.string_interp_validation_altdlc_sourceDirectoryDoesntExist, FriendlyName, AlternateDLCFolder); return; } } else if (MultiListRootPath != null) { foreach (var multif in MultiListSourceFiles) { var path = FilesystemInterposer.PathCombine(modForValidating.IsInArchive, modForValidating.ModPath, MultiListRootPath, multif); if (!FilesystemInterposer.FileExists(path, modForValidating.Archive)) { Log.Error($@"Alternate DLC ({FriendlyName}) specifies a multilist (index {multilistid}) that contains file that does not exist: {multif}"); LoadFailedReason = M3L.GetString(M3L.string_interp_altdlc_multilistMissingFileInMultilist, FriendlyName, multilistid, multif); return; } } } // Validate multilist dlc } var dlcReqs = properties.TryGetValue(@"DLCRequirements", out string _dlcReqs) ? _dlcReqs.Split(';') : null; if (dlcReqs != null) { var reqList = new List <string>(); foreach (var originalReq in dlcReqs) { var testreq = originalReq; string prefix = ""; if (modForValidating.ModDescTargetVersion >= 6.3) { if (testreq.StartsWith("-") || testreq.StartsWith("+")) { prefix = testreq[0].ToString(); } testreq = testreq.TrimStart('-', '+'); } //official headers if (Enum.TryParse(testreq, out ModJob.JobHeader header) && ModJob.GetHeadersToDLCNamesMap(modForValidating.Game).TryGetValue(header, out var foldername)) { reqList.Add(prefix + foldername); continue; } //dlc mods if (!testreq.StartsWith(@"DLC_")) { Log.Error($@"An item in Alternate DLC's ({FriendlyName}) DLCRequirements doesn't start with DLC_ or is not official header. Bad value: {originalReq}"); LoadFailedReason = M3L.GetString(M3L.string_interp_validation_altdlc_dlcRequirementInvalid, FriendlyName, originalReq); return; } else { reqList.Add(originalReq); } } DLCRequirementsForManual = reqList.ToArray(); } if (Condition == AltDLCCondition.COND_SPECIFIC_SIZED_FILES) { var requiredFilePaths = properties.TryGetValue(@"RequiredFileRelativePaths", out string _requiredFilePaths) ? _requiredFilePaths.Split(';').ToList() : new List <string>(); var requiredFileSizes = properties.TryGetValue(@"RequiredFileSizes", out string _requiredFileSizes) ? _requiredFileSizes.Split(';').ToList() : new List <string>(); if (requiredFilePaths.Count() != requiredFileSizes.Count()) { Log.Error($@"Alternate DLC {FriendlyName} uses COND_SPECIFIC_SIZED_FILES but the amount of items in the RequiredFileRelativePaths and RequiredFileSizes lists are not equal"); ValidAlternate = false; LoadFailedReason = M3L.GetString(M3L.string_interp_validation_altdlc_specificSizedFilesMismatchedParams, FriendlyName); return; } for (int i = 0; i < requiredFilePaths.Count(); i++) { var reqFile = requiredFilePaths[i]; var reqSizeStr = requiredFileSizes[i]; if (reqFile.Contains(@"..")) { Log.Error($@"Alternate DLC {FriendlyName} RequiredFileRelativePaths item {reqFile} is invalid: Values cannot contain '..' for security reasons"); ValidAlternate = false; LoadFailedReason = M3L.GetString(M3L.string_interp_validation_altdlc_specificSizedFilesContainsIllegalPattern, FriendlyName, reqFile); return; } reqFile = reqFile.Replace('/', '\\').TrimStart('\\'); //standardize if (long.TryParse(reqSizeStr, out var reqSize) && reqSize >= 0) { RequiredSpecificFiles[reqFile] = reqSize; } else { Log.Error($@"Alternate DLC {FriendlyName} RequiredFileSizes item {reqFile} is invalid: {reqSizeStr}. Values must be greater than or equal to zero."); ValidAlternate = false; LoadFailedReason = M3L.GetString(M3L.string_interp_validation_altdlc_specificSizedFileMustBeLargerThanZero, FriendlyName, reqFile, reqSizeStr); return; } } if (!RequiredSpecificFiles.Any()) { Log.Error($@"Alternate DLC {FriendlyName} is invalid: COND_SPECIFIC_SIZED_FILES is specified as the condition but there are no values in RequiredFileRelativePaths/RequiredFileSizes"); ValidAlternate = false; LoadFailedReason = M3L.GetString(M3L.string_interp_validation_altdlc_specificSizedFilesMissingRequiredParams, FriendlyName); return; } } if (!ReadImageAssetOptions(modForValidating, properties)) { return; // Failed in super call } ReadAutoApplicableText(properties); if (modForValidating.ModDescTargetVersion >= 6.0) { GroupName = properties.TryGetValue(@"OptionGroup", out string groupName) ? groupName : null; } if (Condition == AltDLCCondition.COND_MANUAL && properties.TryGetValue(@"CheckedByDefault", out string checkedByDefault) && bool.TryParse(checkedByDefault, out bool cbd)) { CheckedByDefault = cbd; } if (Condition != AltDLCCondition.COND_MANUAL && Condition != AltDLCCondition.COND_SPECIFIC_SIZED_FILES && Condition != AltDLCCondition.INVALID_CONDITION) { //ensure conditional dlc list has at least one item. if (ConditionalDLC.Count == 0) { Log.Error($@"Alternate DLC {FriendlyName} cannot have empty or missing Conditional DLC list, as it does not use COND_MANUAL or COND_SPECIFIC_SIZED_FILES."); ValidAlternate = false; LoadFailedReason = M3L.GetString(M3L.string_interp_altdlc_emptyConditionalDLCList, FriendlyName); return; } } CLog.Information($@"AlternateDLC loaded and validated: {FriendlyName}", Settings.LogModStartup); ValidAlternate = true; }
private static void RandomizeWeaponIni(DuplicatingIni vanillaFile, DuplicatingIni randomizerIni) { foreach (var section in vanillaFile.Sections) { var sectionsplit = section.Header.Split('.').ToList(); if (sectionsplit.Count > 1) { var objectname = sectionsplit[1]; if (objectname.StartsWith("SFXWeapon_") || objectname.StartsWith("SFXHeavyWeapon_")) { //We can randomize this section of the ini. Debug.WriteLine($"Randomizing weapon {objectname}"); var outSection = randomizerIni.GetOrAddSection(section.Header); foreach (var entry in section.Entries) { if (KeysToNotRandomize.Contains(entry.Key, StringComparer.InvariantCultureIgnoreCase)) { continue; // Do not touch this key } if (entry.HasValue) { // if (entry.Key == "Damage") Debugger.Break(); string value = entry.Value; //range check if (value.StartsWith("(")) { value = value.Substring(0, value.IndexOf(')') + 1); //trim off trash on end (like ; comment ) var p = StringStructParser.GetCommaSplitValues(value); if (p.Count == 2) { try { bool isInt = false; float x = 0; float y = 0; bool isZeroed = false; if (int.TryParse(p["X"].TrimEnd('f'), out var intX) && int.TryParse(p["Y"].TrimEnd('f'), out var intY)) { //integers if (intX < 0 && intY < 0) { Debug.WriteLine($" BELOW ZERO INT: {entry.Key} for {objectname}: {entry.RawText}"); } bool validValue = false; for (int i = 0; i < 10; i++) { bool isMaxMin = intX > intY; //bool isMinMax = intY < intX; isZeroed = intX == 0 && intY == 0; if (isZeroed) { validValue = true; break; } ; //skip bool isSame = intX == intY; bool belowzeroInt = intX < 0 || intY < 0; bool abovezeroInt = intX > 0 || intY > 0; int Max = isMaxMin ? intX : intY; int Min = isMaxMin ? intY : intX; int range = Max - Min; if (range == 0) { range = Max; } if (range == 0) { Debug.WriteLine("Range still 0"); } int rangeExtension = range / 2; //50% int newMin = Math.Max(0, ThreadSafeRandom.Next(Min - rangeExtension, Min + rangeExtension)); int newMax = ThreadSafeRandom.Next(Max - rangeExtension, Max + rangeExtension); intX = isMaxMin ? newMax : newMin; intY = isMaxMin ? newMin : newMax; //might need to check zeros //if (entry.Key.Contains("MagSize")) Debugger.Break(); if (intX != 0 || intY != 0) { x = intX; y = intY; if (isSame) { x = intY; } if (!belowzeroInt && (x <= 0 || y <= 0)) { continue; //not valid. Redo this loop } if (abovezeroInt && (x <= 0 || y <= 0)) { continue; //not valid. Redo this loop } validValue = true; break; //break loop } } if (!validValue) { Debug.WriteLine($"Failed rerolls: {entry.Key} for {objectname}: {entry.RawText}"); } } else { //if (section.Header.Contains("SFXWeapon_GethShotgun")) Debugger.Break(); //floats //Fix error in bioware's coalesced file if (p["X"] == "0.65.0f") { p["X"] = "0.65f"; } float floatx = float.Parse(p["X"].TrimEnd('f')); float floaty = float.Parse(p["Y"].TrimEnd('f')); bool belowzeroFloat = false; if (floatx < 0 || floaty < 0) { Debug.WriteLine($" BELOW ZERO FLOAT: {entry.Key} for {objectname}: {entry.RawText}"); belowzeroFloat = true; } bool isMaxMin = floatx > floaty; bool isMinMax = floatx < floaty; bool isSame = floatx == floaty; isZeroed = floatx == 0 && floaty == 0; if (isZeroed) { continue; } ; //skip float Max = isMaxMin ? floatx : floaty; float Min = isMaxMin ? floaty : floatx; float range = Max - Min; if (range == 0) { range = 0.1f * Max; } float rangeExtension = range * .5f; //50% if (ThreadSafeRandom.Next(10) == 0) { rangeExtension = range * 15f; // Extreme } float newMin = Math.Max(0, ThreadSafeRandom.NextFloat(Min - rangeExtension, Min + rangeExtension)); float newMax = ThreadSafeRandom.NextFloat(Max - rangeExtension, Max + rangeExtension); if (!belowzeroFloat) { //ensure they don't fall below 0 if (newMin < 0) { newMin = Math.Max(newMin, Min / 2); } if (newMax < 0) { newMax = Math.Max(newMax, Max / 2); } //i have no idea what i'm doing } floatx = isMaxMin ? newMax : newMin; floaty = isMaxMin ? newMin : newMax; //might need to check zeros x = floatx; y = floaty; if (isSame) { x = y; } } if (isZeroed) { continue; //skip } // Write out the new value outSection.SetSingleEntry(entry.Key, $"(X={x},Y={y})"); } catch (Exception e) { Log.Error($"Cannot randomize weapon stat {objectname} {entry.Key}: {e.Message}"); } } } else { //Debug.WriteLine(entry.Key); var initialValue = entry.Value.ToString(); var isInt = int.TryParse(entry.Value, out var valueInt); var isFloat = float.TryParse(entry.Value, out var valueFloat); switch (entry.Key) { case "BurstRounds": { var burstMax = valueInt * 2; entry.Value = (ThreadSafeRandom.Next(burstMax) + 1).ToString(); } break; case "RateOfFireAI": case "DamageAI": { entry.Value = ThreadSafeRandom.NextFloat(.1, 2).ToString(CultureInfo.InvariantCulture); } break; case "RecoilInterpSpeed": case "RecoilFadeSpeed": case "RecoilZoomFadeSpeed": case "RecoilYawScale": case "RecoilYawFrequency": case "RecoilYawNoise": case "DamageHench": case "BurstRefireTime": case "ZoomAccFirePenalty": case "ZoomAccFireInterpSpeed": case "FirstHitDamage": case "SecondHitDamage": case "ThirdHitDamage": { entry.Value = ThreadSafeRandom.NextFloat(valueFloat / 2, valueFloat * 1.5).ToString(CultureInfo.InvariantCulture); } break; case "bIsAutomatic": { var curValue = bool.Parse(entry.Value); entry.Value = ThreadSafeRandom.Next(5) == 0 ? (!curValue).ToString() : entry.Value.ToString(); } break; case "MinRefireTime": { entry.Value = ThreadSafeRandom.NextFloat(0.01, 1).ToString(); } break; case "AccFirePenalty": case "AccFireInterpSpeed": { entry.Value = ThreadSafeRandom.NextFloat(0, valueFloat * 1.75).ToString(CultureInfo.InvariantCulture); } break; case "AmmoPerShot": { if (ThreadSafeRandom.Next(10) == 0) { entry.Value = "2"; } // Otherwise do not change } break; case "AIBurstRefireTimeMin": entry.Value = ThreadSafeRandom.NextFloat(0, 2).ToString(CultureInfo.InvariantCulture); break; case "AIBurstRefireTimeMax": entry.Value = ThreadSafeRandom.NextFloat(1, 5).ToString(CultureInfo.InvariantCulture); break; case "MaxSpareAmmo": entry.Value = ThreadSafeRandom.Next(valueInt / 10, valueInt * 2).ToString(CultureInfo.InvariantCulture); break; default: Debug.WriteLine($"Undone key: {entry.Key}"); break; } if (entry.Value != initialValue) { outSection.SetSingleEntry(entry.Key, entry.Value); } } } } // whats this do? //if (section.Entries.All(x => x.Key != "Damage")) //{ // float X = ThreadSafeRandom.NextFloat(2, 7); // float Y = ThreadSafeRandom.NextFloat(2, 7); // section.Entries.Add(new DuplicatingIni.IniEntry($"Damage=(X={X},Y={Y})")); //} } } } }
public AlternateFile(string alternateFileText, ModJob associatedJob, Mod modForValidating) { var properties = StringStructParser.GetCommaSplitValues(alternateFileText); if (properties.TryGetValue(@"FriendlyName", out string friendlyName)) { FriendlyName = friendlyName; } if (modForValidating.ModDescTargetVersion >= 6 && string.IsNullOrWhiteSpace(FriendlyName)) { //Cannot be null. Log.Error(@"Alternate File does not specify FriendlyName. Mods targeting moddesc >= 6.0 cannot have empty FriendlyName"); ValidAlternate = false; LoadFailedReason = M3L.GetString(M3L.string_validation_altfile_oneAltDlcMissingFriendlyNameCmm6); return; } if (!Enum.TryParse(properties[@"Condition"], out Condition)) { Log.Error($@"Alternate File specifies unknown/unsupported condition: {properties[@"Condition"]}"); //do not localize ValidAlternate = false; LoadFailedReason = $@"{M3L.GetString(M3L.string_validation_altfile_unknownCondition)} {properties[@"Condition"]}"; return; } if (properties.TryGetValue(@"ConditionalDLC", out string conditionalDlc)) { var conditionalList = StringStructParser.GetSemicolonSplitList(conditionalDlc); foreach (var dlc in conditionalList) { //if (modForValidating.Game == Mod.MEGame.ME3) //{ if (Enum.TryParse(dlc, out ModJob.JobHeader header) && ModJob.GetHeadersToDLCNamesMap(modForValidating.Game).TryGetValue(header, out var foldername)) { ConditionalDLC.Add(foldername); continue; } //} if (!dlc.StartsWith(@"DLC_")) { Log.Error(@"An item in Alternate Files's ConditionalDLC doesn't start with DLC_"); LoadFailedReason = M3L.GetString(M3L.string_validation_altfile_conditionalDLCInvalidValue, FriendlyName); return; } else { ConditionalDLC.Add(dlc); } } } if (!Enum.TryParse(properties[@"ModOperation"], out Operation)) { Log.Error(@"Alternate File specifies unknown/unsupported operation: " + properties[@"ModOperation"]); ValidAlternate = false; LoadFailedReason = $@"{M3L.GetString(M3L.string_validation_altfile_unknownOperation)} { properties[@"ModOperation"]}"; return; } if (properties.TryGetValue(@"Description", out string description)) { Description = description; } if (modForValidating.ModDescTargetVersion >= 6 && string.IsNullOrWhiteSpace(Description)) { //Cannot be null. Log.Error($@"Alternate File {FriendlyName} with mod targeting moddesc >= 6.0 cannot have empty Description or missing description"); ValidAlternate = false; LoadFailedReason = M3L.GetString(M3L.string_interp_validation_altfile_cmmver6RequiresDescription, FriendlyName); return; } if (Operation != AltFileOperation.OP_NOTHING) { if (properties.TryGetValue(@"ModFile", out string modfile)) { ModFile = modfile.TrimStart('\\', '/').Replace('/', '\\'); } else { Log.Error($@"Alternate file in-mod target (ModFile) required but not specified. This value is required for all Alternate files. Friendlyname: {FriendlyName}"); ValidAlternate = false; LoadFailedReason = M3L.GetString(M3L.string_interp_validation_altfile_noModFileDeclared, FriendlyName); return; } if (associatedJob.Header == ModJob.JobHeader.CUSTOMDLC) { var modFilePath = FilesystemInterposer.PathCombine(modForValidating.IsInArchive, modForValidating.ModPath, ModFile); var pathSplit = ModFile.Split('\\'); if (pathSplit.Length > 0) { var dlcName = pathSplit[0]; var jobKey = associatedJob.CustomDLCFolderMapping.FirstOrDefault(x => x.Value.Equals(dlcName, StringComparison.InvariantCultureIgnoreCase)); if (jobKey.Key != null) { //if (associatedJob.CustomDLCFolderMapping.TryGetValue(ModFile, out var sourceFile)) //{ //} } else { Log.Error($@"Alternate file {FriendlyName} in-mod target (ModFile) does not appear to target a DLC target this mod will (always) install: {ModFile}"); ValidAlternate = false; LoadFailedReason = "Dummy placeholder"; //LoadFailedReason = M3L.GetString(M3L.string_interp_validation_altfile_couldNotFindModFile, FriendlyName, ModFile); return; } } } else { if (!associatedJob.FilesToInstall.TryGetValue(ModFile, out var sourceFile)) { Log.Error($@"Alternate file {FriendlyName} in-mod target (ModFile) specified but does not exist in job: {ModFile}"); ValidAlternate = false; LoadFailedReason = M3L.GetString(M3L.string_interp_validation_altfile_couldNotFindModFile, FriendlyName, ModFile); return; } } //todo: implement multimap if (properties.TryGetValue(@"MultiMappingFile", out string multifilemapping)) { MultiMappingFile = multifilemapping.TrimStart('\\', '/'); } if (properties.TryGetValue(@"AltFile", out string altfile)) { AltFile = altfile; } else if (AltFile == null && properties.TryGetValue(@"ModAltFile", out string maltfile)) { AltFile = maltfile; } properties.TryGetValue(@"SubstituteFile", out SubstituteFile); //Only used in 4.5. In 5.0 and above this became AltFile. //workaround for 4.5 if (modForValidating.ModDescTargetVersion == 4.5 && Operation == AltFileOperation.OP_SUBSTITUTE && SubstituteFile != null) { AltFile = SubstituteFile; } if (!string.IsNullOrEmpty(AltFile)) { AltFile = AltFile.Replace('/', '\\'); //Standardize paths } //This needs reworked from java's hack implementation //Need to identify mods using substitution features if (Operation == AltFileOperation.OP_INSTALL || Operation == AltFileOperation.OP_SUBSTITUTE) { if (MultiMappingFile == null) { //Validate file var altPath = FilesystemInterposer.PathCombine(modForValidating.IsInArchive, modForValidating.ModPath, AltFile); var altFileSourceExists = FilesystemInterposer.FileExists(altPath, modForValidating.Archive); if (!altFileSourceExists) { Log.Error(@"Alternate file source (AltFile) does not exist: " + AltFile); ValidAlternate = false; LoadFailedReason = M3L.GetString(M3L.string_interp_validation_altfile_specifiedAltFileDoesntExist, Operation.ToString(), AltFile); return; } //Ensure it is not part of DLC directory itself. var modFile = FilesystemInterposer.PathCombine(modForValidating.IsInArchive, modForValidating.ModPath, ModFile); //Todo } else { //Multimapping, Todo } } } ApplicableAutoText = properties.TryGetValue(@"ApplicableAutoText", out string applicableText) ? applicableText : M3L.GetString(M3L.string_autoApplied); NotApplicableAutoText = properties.TryGetValue(@"NotApplicableAutoText", out string notApplicableText) ? notApplicableText : M3L.GetString(M3L.string_notApplicable); if (modForValidating.ModDescTargetVersion >= 6.0) { GroupName = properties.TryGetValue(@"OptionGroup", out string groupName) ? groupName : null; } if (Condition == AltFileCondition.COND_MANUAL && properties.TryGetValue(@"CheckedByDefault", out string checkedByDefault) && bool.TryParse(checkedByDefault, out bool cbd)) { CheckedByDefault = cbd; } CLog.Information($@"Alternate file loaded and validated: {FriendlyName}", Settings.LogModStartup); ValidAlternate = true; }
public AlternateDLC(string alternateDLCText, Mod modForValidating) { var properties = StringStructParser.GetCommaSplitValues(alternateDLCText); //todo: if statements to check these. if (properties.TryGetValue(@"FriendlyName", out string friendlyName)) { FriendlyName = friendlyName; } if (modForValidating.ModDescTargetVersion >= 6 && string.IsNullOrWhiteSpace(FriendlyName)) { //Cannot be null. Log.Error(@"Alternate DLC does not specify FriendlyName. Mods targeting moddesc >= 6.0 require FriendlyName"); ValidAlternate = false; LoadFailedReason = M3L.GetString(M3L.string_validation_altdlc_oneAltDlcMissingFriendlyNameCmm6); return; } if (!Enum.TryParse(properties[@"Condition"], out Condition)) { Log.Error($@"Alternate DLC specifies unknown/unsupported condition: {properties[@"Condition"]}"); //do not localize ValidAlternate = false; LoadFailedReason = M3L.GetString(M3L.string_validation_altdlc_unknownCondition) + properties[@"Condition"]; return; } if (!Enum.TryParse(properties[@"ModOperation"], out Operation)) { Log.Error($@"Alternate DLC specifies unknown/unsupported operation: {properties[@"ModOperation"]}"); //do not localize ValidAlternate = false; LoadFailedReason = M3L.GetString(M3L.string_validation_altdlc_unknownOperation) + properties[@"ModOperation"]; return; } if (properties.TryGetValue(@"Description", out string description)) { Description = description; } if (modForValidating.ModDescTargetVersion >= 6 && string.IsNullOrWhiteSpace(Description)) { //Cannot be null. Log.Error($@"Alternate DLC {FriendlyName} cannot have empty Description or missing description as it targets cmmver >= 6"); ValidAlternate = false; LoadFailedReason = M3L.GetString(M3L.string_interp_validation_altdlc_cmmver6RequiresDescription, FriendlyName); return; } if (Operation != AltDLCOperation.OP_NOTHING) { if (properties.TryGetValue(@"ModAltDLC", out string altDLCFolder)) { AlternateDLCFolder = altDLCFolder.Replace('/', '\\'); } else { Log.Error(@"Alternate DLC does not specify ModAltDLC but is required"); ValidAlternate = false; LoadFailedReason = M3L.GetString(M3L.string_interp_validation_altdlc_missingModAltDLC, FriendlyName); return; } if (properties.TryGetValue(@"ModDestDLC", out string destDLCFolder)) { DestinationDLCFolder = destDLCFolder.Replace('/', '\\'); } else { Log.Error(@"Alternate DLC does not specify ModDestDLC but is required"); ValidAlternate = false; LoadFailedReason = M3L.GetString(M3L.string_interp_validation_altdlc_missingModDestDLC, FriendlyName); return; } //todo: Validate target in mod folder if (properties.TryGetValue(@"ConditionalDLC", out string conditionalDlc)) { var conditionalList = StringStructParser.GetSemicolonSplitList(conditionalDlc); foreach (var dlc in conditionalList) { //if (modForValidating.Game == Mod.MEGame.ME3) //{ if (Enum.TryParse(dlc, out ModJob.JobHeader header) && ModJob.GetHeadersToDLCNamesMap(modForValidating.Game).TryGetValue(header, out var foldername)) { ConditionalDLC.Add(foldername); continue; } //} if (!dlc.StartsWith(@"DLC_")) { Log.Error($@"An item in Alternate DLC's ({FriendlyName}) ConditionalDLC doesn't start with DLC_ or is not official header"); LoadFailedReason = M3L.GetString(M3L.string_interp_validation_altdlc_conditionalDLCInvalidValue, FriendlyName); return; } else { ConditionalDLC.Add(dlc); } } } //Validation if (string.IsNullOrWhiteSpace(AlternateDLCFolder)) { Log.Error($@"Alternate DLC directory (ModAltDLC) not specified for { FriendlyName}"); LoadFailedReason = M3L.GetString(M3L.string_interp_validation_altdlc_sourceDirectoryNotSpecifiedForModAltDLC, FriendlyName); return; } if (string.IsNullOrWhiteSpace(DestinationDLCFolder)) { Log.Error($@"Destination DLC directory (ModDestDLC) not specified for {FriendlyName}"); LoadFailedReason = M3L.GetString(M3L.string_interp_validation_altdlc_destinationDirectoryNotSpecifiedForModDestDLC, FriendlyName); return; } AlternateDLCFolder = AlternateDLCFolder.TrimStart('\\', '/').Replace('/', '\\'); //Check ModAltDLC directory exists var localAltDlcDir = FilesystemInterposer.PathCombine(modForValidating.IsInArchive, modForValidating.ModPath, AlternateDLCFolder); if (!FilesystemInterposer.DirectoryExists(localAltDlcDir, modForValidating.Archive)) { Log.Error($@"Alternate DLC directory (ModAltDLC) does not exist: {AlternateDLCFolder}"); LoadFailedReason = M3L.GetString(M3L.string_interp_validation_altdlc_sourceDirectoryDoesntExist, FriendlyName, AlternateDLCFolder); return; } } ApplicableAutoText = properties.TryGetValue(@"ApplicableAutoText", out string applicableText) ? applicableText : M3L.GetString(M3L.string_autoApplied); NotApplicableAutoText = properties.TryGetValue(@"NotApplicableAutoText", out string notApplicableText) ? notApplicableText : M3L.GetString(M3L.string_notApplicable); if (modForValidating.ModDescTargetVersion >= 6.0) { GroupName = properties.TryGetValue(@"OptionGroup", out string groupName) ? groupName : null; } if (Condition == AltDLCCondition.COND_MANUAL && properties.TryGetValue(@"CheckedByDefault", out string checkedByDefault) && bool.TryParse(checkedByDefault, out bool cbd)) { CheckedByDefault = cbd; } CLog.Information($@"AlternateDLC loaded and validated: {FriendlyName}", Settings.LogModStartup); ValidAlternate = true; }
public static void BuildBioPGlobal(GameTarget target) { M3MergeDLC.RemoveMergeDLC(target); var loadedFiles = MELoadedFiles.GetFilesLoadedInGame(target.Game, gameRootOverride: target.TargetPath); //var mergeFiles = loadedFiles.Where(x => // x.Key.StartsWith(@"BioH_") && x.Key.Contains(@"_DLC_MOD_") && x.Key.EndsWith(@".pcc") && !x.Key.Contains(@"_LOC_") && !x.Key.Contains(@"_Explore.")); Log.Information($@"SQMMERGE: Building BioP_Global"); var appearanceInfo = new CaseInsensitiveDictionary <List <SquadmateInfoSingle> >(); int appearanceId = 255; // starting int currentConditional = STARTING_OUTFIT_CONDITIONAL; // Scan squadmate merge files var sqmSupercedances = M3Directories.GetFileSupercedances(target, new[] { @".sqm" }); if (sqmSupercedances.TryGetValue(SQUADMATE_MERGE_MANIFEST_FILE, out var infoList)) { infoList.Reverse(); foreach (var dlc in infoList) { Log.Information($@"SQMMERGE: Processing {dlc}"); var jsonFile = Path.Combine(M3Directories.GetDLCPath(target), dlc, target.Game.CookedDirName(), SQUADMATE_MERGE_MANIFEST_FILE); var infoPackage = JsonConvert.DeserializeObject <SquadmateMergeInfo>(File.ReadAllText(jsonFile)); if (!infoPackage.Validate(dlc, target, loadedFiles)) { continue; // skip this } // Enumerate all outfits listed for a single squadmate foreach (var outfit in infoPackage.Outfits) { List <SquadmateInfoSingle> list; // See if we already have an outfit list for this squadmate, maybe from another mod... if (!appearanceInfo.TryGetValue(outfit.HenchName, out list)) { list = new List <SquadmateInfoSingle>(); appearanceInfo[outfit.HenchName] = list; } outfit.ConditionalIndex = currentConditional++; // This is always incremented, so it might appear out of order in game files depending on how mod order is processed, that should be okay though. outfit.AppearanceId = appearanceId++; // may need adjusted outfit.DLCName = dlc; list.Add(outfit); Log.Information($@"SQMMERGE: ConditionalIndex for {outfit.HenchName} appearanceid {outfit.AppearanceId}: {outfit.ConditionalIndex}"); } } } if (appearanceInfo.Any()) { var biopGlobal = MEPackageHandler.OpenMEPackage(loadedFiles[@"BioP_Global.pcc"]); var lsk = biopGlobal.Exports.FirstOrDefault(x => x.ClassName == @"LevelStreamingKismet"); var persistentLevel = biopGlobal.FindExport(@"TheWorld.PersistentLevel"); // Clone LevelStreamingKismets foreach (var sqm in appearanceInfo.Values) { foreach (var outfit in sqm) { var fName = outfit.HenchPackage; var newLSK = EntryCloner.CloneEntry(lsk); newLSK.WriteProperty(new NameProperty(fName, @"PackageName")); if (target.Game.IsGame3()) { // Game 3 has _Explore files too fName += @"_Explore"; newLSK = EntryCloner.CloneEntry(lsk); newLSK.WriteProperty(new NameProperty(fName, @"PackageName")); } } } // Update BioWorldInfo // Doesn't have consistent number so we can't find it by instanced full path var bioWorldInfo = biopGlobal.Exports.FirstOrDefault(x => x.ClassName == @"BioWorldInfo"); var props = bioWorldInfo.GetProperties(); // Update Plot Streaming var plotStreaming = props.GetProp <ArrayProperty <StructProperty> >(@"PlotStreaming"); foreach (var sqm in appearanceInfo.Values) { foreach (var outfit in sqm) { // find item to add to buildPlotElementObject(plotStreaming, outfit, target.Game, false); if (target.Game.IsGame3()) { buildPlotElementObject(plotStreaming, outfit, target.Game, true); } } } // Update StreamingLevels var streamingLevels = props.GetProp <ArrayProperty <ObjectProperty> >(@"StreamingLevels"); streamingLevels.ReplaceAll(biopGlobal.Exports.Where(x => x.ClassName == @"LevelStreamingKismet").Select(x => new ObjectProperty(x))); bioWorldInfo.WriteProperties(props); M3MergeDLC.GenerateMergeDLC(target, Guid.NewGuid()); // Save BioP_Global into DLC var cookedDir = Path.Combine(M3Directories.GetDLCPath(target), M3MergeDLC.MERGE_DLC_FOLDERNAME, target.Game.CookedDirName()); var outP = Path.Combine(cookedDir, @"BioP_Global.pcc"); biopGlobal.Save(outP); // Generate conditionals file if (target.Game.IsGame3()) { CNDFile cnd = new CNDFile(); cnd.ConditionalEntries = new List <CNDFile.ConditionalEntry>(); foreach (var sqm in appearanceInfo.Values) { foreach (var outfit in sqm) { var scText = $@"(plot.ints[{GetSquadmateOutfitInt(outfit.HenchName, target.Game)}] == i{outfit.MemberAppearanceValue})"; var compiled = ME3ConditionalsCompiler.Compile(scText); cnd.ConditionalEntries.Add(new CNDFile.ConditionalEntry() { Data = compiled, ID = outfit.ConditionalIndex }); } } cnd.ToFile(Path.Combine(cookedDir, $@"Conditionals{M3MergeDLC.MERGE_DLC_FOLDERNAME}.cnd")); } else if (target.Game.IsGame2()) { var startupF = Path.Combine(cookedDir, $@"Startup_{M3MergeDLC.MERGE_DLC_FOLDERNAME}.pcc"); var startup = MEPackageHandler.OpenMEPackageFromStream(Utilities.GetResourceStream( $@"MassEffectModManagerCore.modmanager.mergedlc.{target.Game}.Startup_{M3MergeDLC.MERGE_DLC_FOLDERNAME}.pcc")); var conditionalClass = startup.FindExport($@"PlotManager{M3MergeDLC.MERGE_DLC_FOLDERNAME}.BioAutoConditionals"); // Add Conditional Functions FileLib fl = new FileLib(startup); bool initialized = fl.Initialize(new RelativePackageCache() { RootPath = M3Directories.GetBioGamePath(target) }); if (!initialized) { throw new Exception( $@"FileLib for script update could not initialize, cannot install conditionals"); } var funcToClone = startup.FindExport($@"PlotManager{M3MergeDLC.MERGE_DLC_FOLDERNAME}.BioAutoConditionals.TemplateFunction"); foreach (var sqm in appearanceInfo.Values) { foreach (var outfit in sqm) { var func = EntryCloner.CloneEntry(funcToClone); func.ObjectName = $@"F{outfit.ConditionalIndex}"; func.indexValue = 0; var scText = new StreamReader(Utilities.GetResourceStream( $@"MassEffectModManagerCore.modmanager.squadmates.{target.Game}.HasOutfitOnConditional.txt")) .ReadToEnd(); scText = scText.Replace(@"%CONDITIONALNUM%", outfit.ConditionalIndex.ToString()); scText = scText.Replace(@"%SQUADMATEOUTFITPLOTINT%", outfit.AppearanceId.ToString()); scText = scText.Replace(@"%OUTFITINDEX%", outfit.MemberAppearanceValue.ToString()); (_, MessageLog log) = UnrealScriptCompiler.CompileFunction(func, scText, fl); if (log.AllErrors.Any()) { Log.Error($@"Error compiling function {func.InstancedFullPath}:"); foreach (var l in log.AllErrors) { Log.Error(l.Message); } throw new Exception(M3L.GetString(M3L.string_interp_errorCompilingConditionalFunction, func, string.Join('\n', log.AllErrors.Select(x => x.Message)))); } } } // Relink the conditionals chain UClass uc = ObjectBinary.From <UClass>(conditionalClass); uc.UpdateLocalFunctions(); uc.UpdateChildrenChain(); conditionalClass.WriteBinary(uc); startup.Save(startupF); } // Add startup package, member appearances if (target.Game.IsGame2()) { var bioEngine = Path.Combine(cookedDir, @"BIOEngine.ini"); var ini = DuplicatingIni.LoadIni(bioEngine); var startupSection = ini.GetOrAddSection(@"Engine.StartupPackages"); startupSection.Entries.Add(new DuplicatingIni.IniEntry(@"+DLCStartupPackage", $@"Startup_{M3MergeDLC.MERGE_DLC_FOLDERNAME}")); startupSection.Entries.Add(new DuplicatingIni.IniEntry(@"+Package", $@"PlotManager{M3MergeDLC.MERGE_DLC_FOLDERNAME}")); ini.WriteToFile(bioEngine); } else if (target.Game.IsGame3()) { var mergeCoalFile = Path.Combine(M3Directories.GetDLCPath(target), M3MergeDLC.MERGE_DLC_FOLDERNAME, target.Game.CookedDirName(), $@"Default_{M3MergeDLC.MERGE_DLC_FOLDERNAME}.bin"); var mergeCoal = CoalescedConverter.DecompileGame3ToMemory(new MemoryStream(File.ReadAllBytes(mergeCoalFile))); // Member appearances var bioUiDoc = XDocument.Parse(mergeCoal[@"BIOUI.xml"]); foreach (var sqm in appearanceInfo.Values) { foreach (var outfit in sqm) { var entry = new Game3CoalescedValueEntry() { Section = @"sfxgame.sfxguidata_teamselect", Name = @"selectappearances", Type = 3, Value = StringStructParser.BuildCommaSeparatedSplitValueList(outfit.ToPropertyDictionary(), @"AvailableImage", @"HighlightImage", @"DeadImage", @"SilhouetteImage") }; Game3CoalescedHelper.AddArrayEntry(bioUiDoc, entry); } } mergeCoal[@"BIOUI.xml"] = bioUiDoc.ToString(); // Dynamic load mapping var bioEngineDoc = XDocument.Parse(mergeCoal[@"BIOEngine.xml"]); foreach (var sqm in appearanceInfo.Values) { foreach (var outfit in sqm) { // // * <Section name="sfxgame.sfxengine"> // <Property name="dynamicloadmapping"> // <Value type="3">(ObjectName="BIOG_GesturesConfigDLC.RuntimeData",SeekFreePackageName="GesturesConfigDLC")</Value> var entry = new Game3CoalescedValueEntry() { Section = @"sfxgame.sfxengine", Name = @"dynamicloadmapping", Type = 3 }; entry.Values.Add($"(ObjectName=\"{outfit.AvailableImage}\",SeekFreePackageName=\"SFXHenchImages_{outfit.DLCName}\")"); // do not localize entry.Values.Add($"(ObjectName=\"{outfit.HighlightImage}\",SeekFreePackageName=\"SFXHenchImages_{outfit.DLCName}\")"); // do not localize Game3CoalescedHelper.AddArrayEntry(bioEngineDoc, entry); } } mergeCoal[@"BIOEngine.xml"] = bioEngineDoc.ToString(); CoalescedConverter.CompileFromMemory(mergeCoal).WriteToFile(mergeCoalFile); } } }
public AlternateFile(string alternateFileText, Mod modForValidating) { var properties = StringStructParser.GetCommaSplitValues(alternateFileText); if (properties.TryGetValue("FriendlyName", out string friendlyName)) { FriendlyName = friendlyName; } if (modForValidating.ModDescTargetVersion >= 6 && string.IsNullOrWhiteSpace(FriendlyName)) { //Cannot be null. Log.Error($"Alternate File does not specify FriendlyName. Mods targeting moddesc >= 6.0 cannot have empty FriendlyName"); ValidAlternate = false; LoadFailedReason = $"At least one specified Alternate File does not specify a FriendlyName, which is required for mods targeting cmmver >= 6.0."; return; } if (!Enum.TryParse(properties["Condition"], out Condition)) { Log.Error("Alternate File specifies unknown/unsupported condition: " + properties["Condition"]); ValidAlternate = false; LoadFailedReason = "Alternate File specifies unknown/unsupported condition: " + properties["Condition"]; return; } if (properties.TryGetValue("ConditionalDLC", out string conditionalDlc)) { var conditionalList = StringStructParser.GetSemicolonSplitList(conditionalDlc); foreach (var dlc in conditionalList) { //if (modForValidating.Game == Mod.MEGame.ME3) //{ if (Enum.TryParse(dlc, out ModJob.JobHeader header) && ModJob.GetHeadersToDLCNamesMap(modForValidating.Game).TryGetValue(header, out var foldername)) { ConditionalDLC.Add(foldername); continue; } //} if (!dlc.StartsWith("DLC_")) { Log.Error("An item in Alternate Files's ConditionalDLC doesn't start with DLC_"); LoadFailedReason = $"Alternate File ({FriendlyName}) specifies conditional DLC but no values match the allowed headers or start with DLC_."; return; } else { ConditionalDLC.Add(dlc); } } } if (!Enum.TryParse(properties["ModOperation"], out Operation)) { Log.Error("Alternate File specifies unknown/unsupported operation: " + properties["ModOperation"]); ValidAlternate = false; LoadFailedReason = "Alternate File specifies unknown/unsupported operation: " + properties["ModOperation"]; return; } if (properties.TryGetValue("Description", out string description)) { Description = description; } if (modForValidating.ModDescTargetVersion >= 6 && string.IsNullOrWhiteSpace(Description)) { //Cannot be null. Log.Error($"Alternate File {FriendlyName} with mod targeting moddesc >= 6.0 cannot have empty Description or missing description"); ValidAlternate = false; LoadFailedReason = $"Alternate File {FriendlyName} does not specify a Description, which is required for mods targeting cmmver >= 6.0."; return; } if (properties.TryGetValue("ModFile", out string modfile)) { ModFile = modfile.TrimStart('\\', '/'); } else { Log.Error("Alternate file in-mod target (ModFile) required but not specified. This value is required for all Alternate files"); ValidAlternate = false; LoadFailedReason = $"Alternate file {FriendlyName} does not declare ModFile but it is required for all Alternate Files."; return; } if (properties.TryGetValue("MultiMappingFile", out string multifilemapping)) { MultiMappingFile = multifilemapping.TrimStart('\\', '/'); } if (properties.TryGetValue("AltFile", out string altfile)) { AltFile = altfile; } else if (AltFile == null && properties.TryGetValue("ModAltFile", out string maltfile)) { AltFile = maltfile; } properties.TryGetValue("SubstituteFile", out SubstituteFile); //Only used in 4.5. In 5.0 and above this became AltFile. //workaround for 4.5 if (modForValidating.ModDescTargetVersion == 4.5 && Operation == AltFileOperation.OP_SUBSTITUTE && SubstituteFile != null) { AltFile = SubstituteFile; } if (!string.IsNullOrEmpty(AltFile)) { AltFile = AltFile.Replace('/', '\\'); //Standardize paths } //This needs reworked from java's hack implementation //Need to identify mods using substitution features if (Operation == AltFileOperation.OP_INSTALL || Operation == AltFileOperation.OP_SUBSTITUTE) { if (MultiMappingFile == null) { //Validate file var altPath = FilesystemInterposer.PathCombine(modForValidating.IsInArchive, modForValidating.ModPath, AltFile); var altFileSourceExists = FilesystemInterposer.FileExists(altPath, modForValidating.Archive); if (!altFileSourceExists) { Log.Error("Alternate file source (AltFile) does not exist: " + AltFile); ValidAlternate = false; LoadFailedReason = $"Alternate file is specified with operation {Operation}, but required file doesn't exist: {AltFile}"; return; } //Ensure it is not part of DLC directory itself. var modFile = FilesystemInterposer.PathCombine(modForValidating.IsInArchive, modForValidating.ModPath, ModFile); //Todo } else { //Multimapping, Todo } } if (properties.TryGetValue("ApplicableAutoText", out string applicableText)) { ApplicableAutoText = applicableText; } else { ApplicableAutoText = "Auto Applied"; } if (properties.TryGetValue("NotApplicableAutoText", out string notApplicableText)) { NotApplicableAutoText = notApplicableText; } else { NotApplicableAutoText = "Not applicable"; } if (Condition == AltFileCondition.COND_MANUAL && properties.TryGetValue("CheckedByDefault", out string checkedByDefault) && bool.TryParse(checkedByDefault, out bool cbd)) { CheckedByDefault = cbd; } CLog.Information($"Alternate file loaded and validated: {FriendlyName}", Settings.LogModStartup); ValidAlternate = true; }
public AlternateDLC(string alternateDLCText, Mod modForValidating, ModJob job) { var properties = StringStructParser.GetCommaSplitValues(alternateDLCText); //todo: if statements to check these. if (properties.TryGetValue(@"FriendlyName", out string friendlyName)) { FriendlyName = friendlyName; } if (modForValidating.ModDescTargetVersion >= 6 && string.IsNullOrWhiteSpace(FriendlyName)) { //Cannot be null. Log.Error(@"Alternate DLC does not specify FriendlyName. Mods targeting moddesc >= 6.0 require FriendlyName"); ValidAlternate = false; LoadFailedReason = M3L.GetString(M3L.string_validation_altdlc_oneAltDlcMissingFriendlyNameCmm6); return; } if (!Enum.TryParse(properties[@"Condition"], out Condition)) { Log.Error($@"Alternate DLC specifies unknown/unsupported condition: {properties[@"Condition"]}"); //do not localize ValidAlternate = false; var condition = properties[@"Condition"]; LoadFailedReason = $@"{M3L.GetString(M3L.string_validation_altdlc_unknownCondition)} {condition}"; return; } if (!Enum.TryParse(properties[@"ModOperation"], out Operation)) { Log.Error($@"Alternate DLC specifies unknown/unsupported operation: {properties[@"ModOperation"]}"); //do not localize ValidAlternate = false; var operation = properties[@"ModOperation"]; LoadFailedReason = $@"{M3L.GetString(M3L.string_validation_altdlc_unknownOperation)} {operation}"; return; } if (properties.TryGetValue(@"Description", out string description)) { Description = description; } if (modForValidating.ModDescTargetVersion >= 6 && string.IsNullOrWhiteSpace(Description)) { //Cannot be null. Log.Error($@"Alternate DLC {FriendlyName} cannot have empty Description or missing description as it targets cmmver >= 6"); ValidAlternate = false; LoadFailedReason = M3L.GetString(M3L.string_interp_validation_altdlc_cmmver6RequiresDescription, FriendlyName); return; } //OP_NOTHING can have conditions if (properties.TryGetValue(@"ConditionalDLC", out string conditionalDlc)) { var conditionalList = StringStructParser.GetSemicolonSplitList(conditionalDlc); foreach (var dlc in conditionalList) { //if (modForValidating.Game == Mod.MEGame.ME3) //{ //} if (Condition == AltDLCCondition.COND_SPECIFIC_DLC_SETUP) { //check +/- if (!dlc.StartsWith(@"-") && !dlc.StartsWith(@"+")) { Log.Error($@"An item in Alternate DLC's ({FriendlyName}) ConditionalDLC doesn't start with + or -. When using the condition {Condition}, you must precede DLC names with + or -. Bad value: {dlc}"); LoadFailedReason = $"An item in Alternate DLC's ({FriendlyName}) ConditionalDLC doesn't start with + or -. When using the condition {Condition}, you must precede DLC names with + or -. Bad value: {dlc}"; return; } var prefix = dlc.Substring(0, 1); var realname = dlc.Substring(1); //official headers if (Enum.TryParse(realname, out ModJob.JobHeader header) && ModJob.GetHeadersToDLCNamesMap(modForValidating.Game).TryGetValue(header, out var foldername)) { ConditionalDLC.Add(prefix + foldername); continue; } //dlc mods if (!realname.StartsWith(@"DLC_")) { Log.Error($@"An item in Alternate DLC's ({FriendlyName}) ConditionalDLC doesn't start with DLC_ or is not official header (after the +/- required by {Condition}). Bad value: {dlc}"); LoadFailedReason = $"An item in Alternate DLC's ({FriendlyName}) ConditionalDLC doesn't start with DLC_ or is not official header (after the +/- required by {Condition}). Bad value: {dlc}"; return; } else { ConditionalDLC.Add(prefix + realname); } } else { if (Enum.TryParse(dlc, out ModJob.JobHeader header) && ModJob.GetHeadersToDLCNamesMap(modForValidating.Game).TryGetValue(header, out var foldername)) { ConditionalDLC.Add(foldername); continue; } if (!dlc.StartsWith(@"DLC_")) { Log.Error($@"An item in Alternate DLC's ({FriendlyName}) ConditionalDLC doesn't start with DLC_ or is not official header"); LoadFailedReason = M3L.GetString(M3L.string_interp_validation_altdlc_conditionalDLCInvalidValue, FriendlyName); return; } else { ConditionalDLC.Add(dlc); } } } } if (Operation != AltDLCOperation.OP_NOTHING) { int multilistid = -1; if (Operation == AltDLCOperation.OP_ADD_MULTILISTFILES_TO_CUSTOMDLC) { if (properties.TryGetValue(@"MultiListRootPath", out var rootpath)) { MultiListRootPath = rootpath.TrimStart('\\', '/').Replace('/', '\\'); } else { Log.Error($@"Alternate DLC ({FriendlyName}) specifies operation OP_ADD_MULTILISTFILES_TO_CUSTOMDLC but does not specify the required item MultiListRootPath."); ValidAlternate = false; LoadFailedReason = M3L.GetString(M3L.string_interp_altdlc_multilistMissingMultiListRootPath, FriendlyName); return; } if (properties.TryGetValue(@"MultiListId", out string multilistidstr) && int.TryParse(multilistidstr, out multilistid)) { if (job.MultiLists.TryGetValue(multilistid, out var ml)) { MultiListSourceFiles = ml; } else { Log.Error($@"Alternate DLC ({FriendlyName}) Multilist ID does not exist as part of the task: multilist" + multilistid); ValidAlternate = false; var id = @"multilist" + multilistid; LoadFailedReason = M3L.GetString(M3L.string_interp_altdlc_multilistMissingMultiListX, FriendlyName, id); return; } } else { Log.Error($@"Alternate DLC ({FriendlyName}) specifies operation OP_ADD_MULTILISTFILES_TO_CUSTOMDLC but does not specify the MultiListId attribute, or it could not be parsed to an integer."); ValidAlternate = false; LoadFailedReason = M3L.GetString(M3L.string_interp_altdlc_multilistIdNotIntegerOrMissing, FriendlyName); return; } } else { if (properties.TryGetValue(@"ModAltDLC", out string altDLCFolder)) { AlternateDLCFolder = altDLCFolder.Replace('/', '\\'); } else { Log.Error(@"Alternate DLC does not specify ModAltDLC but is required"); ValidAlternate = false; LoadFailedReason = M3L.GetString(M3L.string_interp_validation_altdlc_missingModAltDLC, FriendlyName); return; } } if (properties.TryGetValue(@"ModDestDLC", out string destDLCFolder)) { DestinationDLCFolder = destDLCFolder.Replace('/', '\\'); } else { Log.Error(@"Alternate DLC does not specify ModDestDLC but is required"); ValidAlternate = false; LoadFailedReason = M3L.GetString(M3L.string_interp_validation_altdlc_missingModDestDLC, FriendlyName); return; } //todo: Validate target in mod folder //Validation if (string.IsNullOrWhiteSpace(AlternateDLCFolder) && MultiListRootPath == null) { Log.Error($@"Alternate DLC directory (ModAltDLC) not specified for { FriendlyName}"); LoadFailedReason = M3L.GetString(M3L.string_interp_validation_altdlc_sourceDirectoryNotSpecifiedForModAltDLC, FriendlyName); return; } if (string.IsNullOrWhiteSpace(DestinationDLCFolder)) { Log.Error($@"Destination DLC directory (ModDestDLC) not specified for {FriendlyName}"); LoadFailedReason = M3L.GetString(M3L.string_interp_validation_altdlc_destinationDirectoryNotSpecifiedForModDestDLC, FriendlyName); return; } if (AlternateDLCFolder != null) { AlternateDLCFolder = AlternateDLCFolder.TrimStart('\\', '/').Replace('/', '\\'); //Check ModAltDLC directory exists var localAltDlcDir = FilesystemInterposer.PathCombine(modForValidating.IsInArchive, modForValidating.ModPath, AlternateDLCFolder); if (!FilesystemInterposer.DirectoryExists(localAltDlcDir, modForValidating.Archive)) { Log.Error($@"Alternate DLC directory (ModAltDLC) does not exist: {AlternateDLCFolder}"); LoadFailedReason = M3L.GetString(M3L.string_interp_validation_altdlc_sourceDirectoryDoesntExist, FriendlyName, AlternateDLCFolder); return; } } else if (MultiListRootPath != null) { foreach (var multif in MultiListSourceFiles) { var path = FilesystemInterposer.PathCombine(modForValidating.IsInArchive, modForValidating.ModPath, MultiListRootPath, multif); if (!FilesystemInterposer.FileExists(path, modForValidating.Archive)) { Log.Error($@"Alternate DLC ({FriendlyName}) specifies a multilist (index {multilistid}) that contains file that does not exist: {multif}"); LoadFailedReason = M3L.GetString(M3L.string_interp_altdlc_multilistMissingFileInMultilist, FriendlyName, multilistid, multif); return; } } } } DLCRequirementsForManual = properties.TryGetValue(@"DLCRequirements", out string dlcReqs) ? dlcReqs.Split(';') : null; if (Condition == AltDLCCondition.COND_SPECIFIC_SIZED_FILES) { var requiredFilePaths = properties.TryGetValue(@"RequiredFileRelativePaths", out string _requiredFilePaths) ? _requiredFilePaths.Split(';').ToList() : new List <string>(); var requiredFileSizes = properties.TryGetValue(@"RequiredFileSizes", out string _requiredFileSizes) ? _requiredFileSizes.Split(';').ToList() : new List <string>(); if (requiredFilePaths.Count() != requiredFileSizes.Count()) { Log.Error($@"Alternate DLC {FriendlyName} uses COND_SPECIFIC_SIZED_FILES but the amount of items in the RequiredFileRelativePaths and RequiredFileSizes lists are not equal"); ValidAlternate = false; LoadFailedReason = $"Alternate DLC {FriendlyName} uses COND_SPECIFIC_SIZED_FILES but the amount of items in the RequiredFileRelativePaths and RequiredFileSizes lists are not equal"; return; } for (int i = 0; i < requiredFilePaths.Count(); i++) { var reqFile = requiredFilePaths[i]; var reqSizeStr = requiredFileSizes[i]; if (reqFile.Contains(@"..")) { Log.Error($@"Alternate DLC {FriendlyName} RequiredFileRelativePaths item {reqFile} is invalid: Values cannot contain '..' for security reasons"); ValidAlternate = false; LoadFailedReason = $"Alternate DLC {FriendlyName} RequiredFileRelativePaths item {reqFile} is invalid: Values cannot contain '..' for security reasons"; return; } if (long.TryParse(reqSizeStr, out var reqSize) && reqSize >= 0) { RequiredSpecificFiles[reqFile] = reqSize; } else { Log.Error($@"Alternate DLC {FriendlyName} RequiredFileSizes item {reqFile} is invalid: {reqSizeStr}. Values must be greater than or equal to zero."); ValidAlternate = false; LoadFailedReason = $"Alternate DLC {FriendlyName} RequiredFileSizes item {reqFile} is invalid: {reqSizeStr}. Values must be greater than or equal to zero."; return; } } if (!RequiredSpecificFiles.Any()) { Log.Error($@"Alternate DLC {FriendlyName} is invalid: COND_SPECIFIC_SIZED_FILES is specified as the condition but there are no values in RequiredFileRelativePaths/RequiredFileSizes"); ValidAlternate = false; LoadFailedReason = $"Alternate DLC {FriendlyName} is invalid: COND_SPECIFIC_SIZED_FILES is specified as the condition but there are no values in RequiredFileRelativePaths/RequiredFileSizes"; return; } } ApplicableAutoText = properties.TryGetValue(@"ApplicableAutoText", out string applicableText) ? applicableText : M3L.GetString(M3L.string_autoApplied); NotApplicableAutoText = properties.TryGetValue(@"NotApplicableAutoText", out string notApplicableText) ? notApplicableText : M3L.GetString(M3L.string_notApplicable); if (modForValidating.ModDescTargetVersion >= 6.0) { GroupName = properties.TryGetValue(@"OptionGroup", out string groupName) ? groupName : null; //TODO: FORCE OPTIONGROUP TO HAVE ONE ITEM CHECKEDBYDFEAULT. HAVE TO CHECK AT HIGHER LEVEL IN PARSER } if (Condition == AltDLCCondition.COND_MANUAL && properties.TryGetValue(@"CheckedByDefault", out string checkedByDefault) && bool.TryParse(checkedByDefault, out bool cbd)) { CheckedByDefault = cbd; } if (Condition != AltDLCCondition.COND_MANUAL && Condition != AltDLCCondition.INVALID_CONDITION) { //ensure conditional dlc list has at least one item. if (ConditionalDLC.Count == 0) { Log.Error($@"Alternate DLC {FriendlyName} cannot have empty or missing Conditional DLC list, as it does not use COND_MANUAL."); ValidAlternate = false; LoadFailedReason = M3L.GetString(M3L.string_interp_altdlc_emptyConditionalDLCList, FriendlyName); return; } } CLog.Information($@"AlternateDLC loaded and validated: {FriendlyName}", Settings.LogModStartup); ValidAlternate = true; }
public AlternateDLC(string alternateDLCText, Mod modForValidating) { var properties = StringStructParser.GetCommaSplitValues(alternateDLCText); //todo: if statements to check these. if (properties.TryGetValue("FriendlyName", out string friendlyName)) { FriendlyName = friendlyName; } if (modForValidating.ModDescTargetVersion >= 6 && string.IsNullOrWhiteSpace(FriendlyName)) { //Cannot be null. Log.Error($"Alternate DLC does not specify FriendlyName. Mods targeting moddesc >= 6.0 require FriendlyName"); ValidAlternate = false; LoadFailedReason = $"At least one specified Alternate DLC does not specify a FriendlyName, which is required for mods targeting cmmver >= 6.0."; return; } if (!Enum.TryParse(properties["Condition"], out Condition)) { Log.Error("Alternate DLC specifies unknown/unsupported condition: " + properties["Condition"]); ValidAlternate = false; LoadFailedReason = "Alternate DLC specifies unknown/unsupported condition: " + properties["Condition"]; return; } if (!Enum.TryParse(properties["ModOperation"], out Operation)) { Log.Error("Alternate DLC specifies unknown/unsupported operation: " + properties["ModOperation"]); ValidAlternate = false; LoadFailedReason = "Alternate DLC specifies unknown/unsupported operation: " + properties["ModOperation"]; return; } if (properties.TryGetValue("Description", out string description)) { Description = description; } if (modForValidating.ModDescTargetVersion >= 6 && string.IsNullOrWhiteSpace(Description)) { //Cannot be null. Log.Error($"Alternate DLC {FriendlyName} cannot have empty Description or missing description as it targets cmmver >= 6"); ValidAlternate = false; LoadFailedReason = $"Alternate DLC {FriendlyName} does not specify a Description, which is required for mods targeting cmmver >= 6.0."; return; } if (properties.TryGetValue("ModAltDLC", out string altDLCFolder)) { AlternateDLCFolder = altDLCFolder.Replace('/', '\\'); } else { Log.Error("Alternate DLC does not specify ModAltDLC but is required"); ValidAlternate = false; LoadFailedReason = $"Alternate DLC {FriendlyName} does not declare ModAltDLC but it is required for all Alternate DLC."; return; } if (properties.TryGetValue("ModDestDLC", out string destDLCFolder)) { DestinationDLCFolder = destDLCFolder.Replace('/', '\\'); } else { Log.Error("Alternate DLC does not specify ModDestDLC but is required"); ValidAlternate = false; LoadFailedReason = $"Alternate DLC {FriendlyName} does not declare ModDestDLC but it is required for all Alternate DLC."; return; } //todo: Validate target in mod folder if (properties.TryGetValue("ConditionalDLC", out string conditionalDlc)) { var conditionalList = StringStructParser.GetSemicolonSplitList(conditionalDlc); foreach (var dlc in conditionalList) { //if (modForValidating.Game == Mod.MEGame.ME3) //{ if (Enum.TryParse(dlc, out ModJob.JobHeader header) && ModJob.GetHeadersToDLCNamesMap(modForValidating.Game).TryGetValue(header, out var foldername)) { ConditionalDLC.Add(foldername); continue; } //} if (!dlc.StartsWith("DLC_")) { Log.Error("An item in Alternate DLC's ConditionalDLC doesn't start with DLC_"); LoadFailedReason = $"Alternate DLC ({FriendlyName}) specifies conditional DLC but no values match the allowed headers or start with DLC_."; return; } else { ConditionalDLC.Add(dlc); } } } if (properties.TryGetValue("ApplicableAutoText", out string applicableText)) { ApplicableAutoText = applicableText; } else { ApplicableAutoText = "Auto Applied"; } if (properties.TryGetValue("NotApplicableAutoText", out string notApplicableText)) { NotApplicableAutoText = notApplicableText; } else { NotApplicableAutoText = "Not applicable"; } if (Condition == AltDLCCondition.COND_MANUAL && properties.TryGetValue("CheckedByDefault", out string checkedByDefault) && bool.TryParse(checkedByDefault, out bool cbd)) { CheckedByDefault = cbd; } //Validation if (string.IsNullOrWhiteSpace(AlternateDLCFolder)) { Log.Error("Alternate DLC directory (ModAltDLC) not specified"); LoadFailedReason = $"Alternate DLC for AltDLC ({FriendlyName}) is specified, but source directory (ModAltDLC) was not specified."; return; } if (string.IsNullOrWhiteSpace(DestinationDLCFolder)) { Log.Error("Destination DLC directory (ModDestDLC) not specified"); LoadFailedReason = $"Destination DLC for AltDLC ({FriendlyName}) is specified, but source directory (ModDestDLC) was not specified."; return; } AlternateDLCFolder = AlternateDLCFolder.TrimStart('\\', '/').Replace('/', '\\'); //Check ModAltDLC directory exists var localAltDlcDir = FilesystemInterposer.PathCombine(modForValidating.IsInArchive, modForValidating.ModPath, AlternateDLCFolder); if (!FilesystemInterposer.DirectoryExists(localAltDlcDir, modForValidating.Archive)) { Log.Error("Alternate DLC directory (ModAltDLC) does not exist: " + AlternateDLCFolder); LoadFailedReason = $"Alternate DLC ({FriendlyName}) is specified, but source for alternate DLC directory does not exist: {AlternateDLCFolder}"; return; } CLog.Information($"AlternateDLC loaded and validated: {FriendlyName}", Settings.LogModStartup); ValidAlternate = true; }