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 static bool RunPlotManagerUpdate(GameTarget target) { Log.Information($@"Updating PlotManager for game: {target.TargetPath}"); var supercedances = M3Directories.GetFileSupercedances(target, new[] { @".pmu" }); Dictionary <string, string> funcMap = new(); List <string> combinedNames = new List <string>(); if (supercedances.TryGetValue(@"PlotManagerUpdate.pmu", out var supercedanes)) { supercedanes.Reverse(); // list goes from highest to lowest. We want to build in lowest to highest StringBuilder sb = null; string currentFuncNum = null; var metaMaps = M3Directories.GetMetaMappedInstalledDLC(target, false); foreach (var pmuDLCName in supercedanes) { var uiName = metaMaps[pmuDLCName]?.ModName ?? ThirdPartyServices.GetThirdPartyModInfo(pmuDLCName, target.Game)?.modname ?? pmuDLCName; combinedNames.Add(uiName); var text = File.ReadAllLines(Path.Combine(M3Directories.GetDLCPath(target), pmuDLCName, target.Game.CookedDirName(), @"PlotManagerUpdate.pmu")); foreach (var line in text) { if (line.StartsWith(@"public function bool F")) { if (sb != null) { funcMap[currentFuncNum] = sb.ToString(); Log.Information($@"PlotSync: Adding function {currentFuncNum} from {pmuDLCName}"); currentFuncNum = null; } sb = new StringBuilder(); sb.AppendLine(line); // Method name currentFuncNum = line.Substring(22); currentFuncNum = currentFuncNum.Substring(0, currentFuncNum.IndexOf('(')); if (int.TryParse(currentFuncNum, out var num)) { if (num <= 0) { Log.Error($@"Skipping plot manager update: Conditional {num} is not a valid number for use. Values must be greater than 0 and less than 2 billion."); Analytics.TrackEvent(@"Bad plot manager function", new Dictionary <string, string>() { { @"FunctionName", $@"F{currentFuncNum}" }, { @"DLCName", pmuDLCName } }); sb = null; return(false); } else if (num.ToString().Length != currentFuncNum.Length) { Log.Error($@"Skipping plot manager update: Conditional {currentFuncNum} is not a valid number for use. Values must not contain leading zeros"); Analytics.TrackEvent(@"Bad plot manager function", new Dictionary <string, string>() { { @"FunctionName", $@"F{currentFuncNum}" }, { @"DLCName", pmuDLCName } }); sb = null; return(false); } } else { Log.Error($@"Skipping plot manager update: Conditional {currentFuncNum} is not a valid number for use. Values must be greater than 0 and less than 2 billion."); Analytics.TrackEvent(@"Bad plot manager function", new Dictionary <string, string>() { { @"FunctionName", $@"F{currentFuncNum}" }, { @"DLCName", pmuDLCName } }); sb = null; return(false); } } else { sb?.AppendLine(line); } } // Add final, if any was found if (sb != null) { funcMap[currentFuncNum] = sb.ToString(); Log.Information($@"PlotSync: Adding function {currentFuncNum} from {pmuDLCName}"); } } } var pmPath = GetPlotManagerPath(target); var vpm = Utilities.ExtractInternalFileToStream($@"MassEffectModManagerCore.modmanager.plotmanager.{target.Game}.PlotManager.{(target.Game == MEGame.ME1 ? @"u" : @"pcc")}"); // do not localize if (funcMap.Any()) { var plotManager = MEPackageHandler.OpenMEPackageFromStream(vpm, $@"PlotManager.{(target.Game == MEGame.ME1 ? @"u" : @"pcc")}"); // do not localize var clonableFunction = plotManager.Exports.FirstOrDefault(x => x.ClassName == @"Function"); // STEP 1: ADD ALL NEW FUNCTIONS BEFORE WE INITIALIZE THE FILELIB. foreach (var v in funcMap) { var pmKey = $@"BioAutoConditionals.F{v.Key}"; var exp = plotManager.FindExport(pmKey); if (exp == null) { // Adding a new conditional exp = EntryCloner.CloneEntry(clonableFunction); exp.ObjectName = new NameReference($@"F{v.Key}", 0); exp.FileRef.InvalidateLookupTable(); // We changed the name. // Reduces trash UFunction uf = ObjectBinary.From <UFunction>(exp); uf.Children = 0; uf.ScriptBytes = Array.Empty <byte>(); // No script data exp.WriteBinary(uf); Log.Information($@"Generated new blank conditional function export: {exp.UIndex} {exp.InstancedFullPath}", Settings.LogModInstallation); } } // Relink child chain UClass uc = ObjectBinary.From <UClass>(plotManager.FindExport(@"BioAutoConditionals")); uc.UpdateChildrenChain(); uc.UpdateLocalFunctions(); uc.Export.WriteBinary(uc); // STEP 2: UPDATE FUNCTIONS Stopwatch sw = Stopwatch.StartNew(); var fl = new FileLib(plotManager); bool initialized = fl.Initialize(new RelativePackageCache() { RootPath = M3Directories.GetBioGamePath(target) }, target.TargetPath, canUseBinaryCache: false); if (!initialized) { Log.Error(@"Error initializing FileLib for plot manager sync:"); foreach (var v in fl.InitializationLog.AllErrors) { Log.Error(v.Message); } throw new Exception(M3L.GetString(M3L.string_interp_fileLibInitFailedPlotManager, string.Join(Environment.NewLine, fl.InitializationLog.AllErrors.Select(x => x.Message)))); //force localize } sw.Stop(); Debug.WriteLine($@"Took {sw.ElapsedMilliseconds}ms to load filelib"); bool relinkChain = false; foreach (var v in funcMap) { var pmKey = $@"BioAutoConditionals.F{v.Key}"; Log.Information($@"Updating conditional entry: {pmKey}", Settings.LogModInstallation); var exp = plotManager.FindExport(pmKey); (_, MessageLog log) = UnrealScriptCompiler.CompileFunction(exp, v.Value, fl); if (log.AllErrors.Any()) { Log.Error($@"Error compiling function {exp.InstancedFullPath}:"); foreach (var l in log.AllErrors) { Log.Error(l.Message); } throw new Exception(M3L.GetString(M3L.string_interp_errorCompilingFunctionReason, exp, string.Join('\n', log.AllErrors.Select(x => x.Message)))); } } if (plotManager.IsModified) { plotManager.Save(pmPath, true); // Update local file DB var bgfe = new BasegameFileIdentificationService.BasegameCloudDBFile(pmPath.Substring(target.TargetPath.Length + 1), (int)new FileInfo(pmPath).Length, target.Game, M3L.GetString(M3L.string_interp_plotManagerSyncForX, string.Join(@", ", combinedNames)), Utilities.CalculateMD5(pmPath)); BasegameFileIdentificationService.AddLocalBasegameIdentificationEntries(new List <BasegameFileIdentificationService.BasegameCloudDBFile>(new[] { bgfe })); } } else { // Just write out vanilla. vpm.WriteToFile(pmPath); } return(true); }