private static void ChangePrisonerNames() { InstallName(342079); // Prisoner 780 InstallName(342078); // Prisoner 403 var didPrisoner = InstallName(BeatPrisonerTLKID) != 0; // Beat Prisoner var didGuard = InstallName(BeatPrisonerGuardTLKID) != 0; // Beating Guard if (didGuard && didPrisoner) { // Make it so the beating scene shows names var cellBLock3F = MERFileSystem.GetPackageFile("BioD_PrsCvA_103CellBlock03.pcc"); if (cellBLock3F != null) { var cellBlock3P = MEPackageHandler.OpenMEPackage(cellBLock3F); // Clone the turianguard pawn type so we can change the name, maybe something else if we want var newGuardBPCST = EntryCloner.CloneTree(cellBlock3P.GetUExport(701), true); newGuardBPCST.ObjectName = "MER_NamedBeatGuard"; newGuardBPCST.WriteProperty(new StringRefProperty(BeatPrisonerGuardTLKID, "ActorGameNameStrRef")); cellBlock3P.GetUExport(668).WriteProperty(new ObjectProperty(newGuardBPCST, "ActorType")); // Change shown name for the prisoner cellBlock3P.GetUExport(699).WriteProperty(new StringRefProperty(BeatPrisonerTLKID, "ActorGameNameStrRef")); // Make the two people 'selectable' so they show up with names cellBlock3P.GetUExport(682).RemoveProperty("m_bTargetableOverride"); // guard cellBlock3P.GetUExport(677).RemoveProperty("m_bTargetableOverride"); // prisoner MERFileSystem.SavePackage(cellBlock3P); } } }
private static void MakeTubesSectionHarder() { var preReaperF = MERFileSystem.GetPackageFile("BioD_EndGm2_420CombatZone.pcc"); if (preReaperF != null && File.Exists(preReaperF)) { var preReaperP = MEPackageHandler.OpenMEPackage(preReaperF); // Open tubes on kills to start the attack (post platforms)---------------------- var seq = preReaperP.GetUExport(15190); var attackSw = MERSeqTools.InstallRandomSwitchIntoSequence(seq, 2); //50% chance // killed squad member -> squad still exists to 50/50 sw KismetHelper.CreateOutputLink(preReaperP.GetUExport(15298), "SquadStillExists", attackSw); // 50/50 to just try to do reaper attack KismetHelper.CreateOutputLink(attackSw, "Link 1", preReaperP.GetUExport(14262)); // Automate the platforms one after another KismetHelper.RemoveAllLinks(preReaperP.GetUExport(15010)); //B Plat01 Death KismetHelper.RemoveAllLinks(preReaperP.GetUExport(15011)); //B Plat02 Death // Sub automate - Remove attack completion gate inputs ---- KismetHelper.RemoveAllLinks(preReaperP.GetUExport(15025)); //Plat03 Attack complete KismetHelper.RemoveAllLinks(preReaperP.GetUExport(15029)); //Plat02 Attack complete //// Sub automate - Remove activate input into gate var cmb2activated = preReaperP.GetUExport(15082); var cmb3activated = preReaperP.GetUExport(15087); KismetHelper.RemoveAllLinks(cmb2activated); KismetHelper.CreateOutputLink(cmb2activated, "Out", preReaperP.GetUExport(2657)); // Delay the start of platform 3 by 4 seconds to give player a bit more time to handle first two platforms // Player will likely have decent weapons by now so they will be better than my testing for sure KismetHelper.RemoveAllLinks(cmb3activated); var newDelay = EntryCloner.CloneEntry(preReaperP.GetUExport(14307)); newDelay.WriteProperty(new FloatProperty(4, "Duration")); KismetHelper.AddObjectToSequence(newDelay, preReaperP.GetUExport(15183), true); KismetHelper.CreateOutputLink(cmb3activated, "Out", newDelay); KismetHelper.CreateOutputLink(newDelay, "Finished", preReaperP.GetUExport(2659)); // preReaperP.GetUExport(14451).RemoveProperty("bOpen"); // Plat03 gate - forces gate open so when reaper attack fires it passes through // preReaperP.GetUExport(14450).RemoveProperty("bOpen"); // Plat02 gate - forces gate open so when reaper attack fires it passes through // There is no end to Plat03 behavior until tubes are dead KismetHelper.CreateOutputLink(preReaperP.GetUExport(14469), "Completed", preReaperP.GetUExport(14374)); // Interp completed to Complete in Plat01 KismetHelper.CreateOutputLink(preReaperP.GetUExport(14470), "Completed", preReaperP.GetUExport(14379)); // Interp completed to Complete in Plat02 // if possession fails continue the possession loop on plat3 to end of pre-reaper combat KismetHelper.CreateOutputLink(preReaperP.GetUExport(16414), "Failed", preReaperP.GetUExport(14307)); MERFileSystem.SavePackage(preReaperP); } }
//me3 pcc to augment must be a map, and must have at least one BioTriggerStream and LevelStreamingKismet static void AugmentMapToLoadLiveEditor(IMEPackage pcc) { const string stateName = "SS_LIVEEDITOR"; var mainSequence = pcc.Exports.First(exp => exp.ObjectName == "Main_Sequence" && exp.ClassName == "Sequence"); var bioWorldInfo = pcc.Exports.First(exp => exp.ClassName == "BioWorldInfo"); #region Sequencing var consoleEvent = SequenceObjectCreator.CreateSequenceObject(pcc, "SeqEvent_Console", MEGame.ME3); consoleEvent.WriteProperty(new NameProperty("LoadLiveEditor", "ConsoleEventName")); KismetHelper.AddObjectToSequence(consoleEvent, mainSequence); var setStreamingState = SequenceObjectCreator.CreateSequenceObject(pcc, "BioSeqAct_SetStreamingState", MEGame.ME3); setStreamingState.WriteProperty(new NameProperty(stateName, "StateName")); setStreamingState.WriteProperty(new BoolProperty(true, "NewValue")); KismetHelper.AddObjectToSequence(setStreamingState, mainSequence); KismetHelper.CreateOutputLink(consoleEvent, "Out", setStreamingState); var levelLoaded = SequenceObjectCreator.CreateSequenceObject(pcc, "SeqEvent_LevelLoaded", MEGame.ME3); KismetHelper.AddObjectToSequence(levelLoaded, mainSequence); var sendMessageToME3Exp = SequenceObjectCreator.CreateSequenceObject(pcc, "SeqAct_SendMessageToME3Explorer", MEGame.ME3); KismetHelper.AddObjectToSequence(sendMessageToME3Exp, mainSequence); KismetHelper.CreateOutputLink(levelLoaded, "Loaded and Visible", sendMessageToME3Exp); var stringVar = SequenceObjectCreator.CreateSequenceObject(pcc, "SeqVar_String", MEGame.ME3); stringVar.WriteProperty(new StrProperty(LoaderLoadedMessage, "StrValue")); KismetHelper.AddObjectToSequence(stringVar, mainSequence); KismetHelper.CreateVariableLink(sendMessageToME3Exp, "MessageName", stringVar); #endregion ExportEntry lsk = EntryCloner.CloneEntry(pcc.Exports.First(exp => exp.ClassName == "LevelStreamingKismet")); lsk.WriteProperty(new NameProperty(liveEditorFileName, "PackageName")); var streamingLevels = bioWorldInfo.GetProperty <ArrayProperty <ObjectProperty> >("StreamingLevels"); streamingLevels.Add(new ObjectProperty(lsk)); bioWorldInfo.WriteProperty(streamingLevels); ExportEntry bts = EntryCloner.CloneTree(pcc.Exports.First(exp => exp.ClassName == "BioTriggerStream")); var streamingStates = bts.GetProperty <ArrayProperty <StructProperty> >("StreamingStates"); while (streamingStates.Count > 1) { streamingStates.RemoveAt(streamingStates.Count - 1); } streamingStates.Add(new StructProperty("BioStreamingState", new PropertyCollection { new NameProperty(stateName, "StateName"), new ArrayProperty <NameProperty>("VisibleChunkNames") { new NameProperty(liveEditorFileName) } })); bts.WriteProperty(streamingStates); pcc.AddToLevelActorsIfNotThere(bts); }
private static void GateTubesAttack() { // This doesn't actually work like expected, it seems gate doesn't store input value // Adds a gate to the tubes attack to ensure it doesn't fire while the previous attack is running still. var tubesF = MERFileSystem.GetPackageFile("BioD_EndGm2_425ReaperTubes.pcc"); if (tubesF != null && File.Exists(tubesF)) { var tubesP = MEPackageHandler.OpenMEPackage(tubesF); // Clone a gate var gateToClone = tubesP.GetUExport(1316); var seq = tubesP.GetUExport(1496); var newGate = EntryCloner.CloneEntry(gateToClone); newGate.RemoveProperty("bOpen"); // Make it open by default. KismetHelper.AddObjectToSequence(newGate, seq, true); // Hook up the 'START REAPER ATTACK' to the gate, remove it's existing output. var sraEvent = tubesP.GetUExport(1455); KismetHelper.RemoveOutputLinks(sraEvent); KismetHelper.CreateOutputLink(sraEvent, "Out", newGate, 0); // 0 = in, which means fire or queue for fire // Hook up the ending of the attack to the gate for 'open' so the gate can be passed through. var delay = tubesP.GetUExport(1273); KismetHelper.CreateOutputLink(tubesP.GetUExport(103), "Out", delay); // Attack finished (CameraShake_Intimidate) to 2s delay KismetHelper.RemoveAllLinks(delay); KismetHelper.CreateOutputLink(delay, "Finished", newGate, 1); //2s Delay to open gate // Make the gate automatically close itself on pass through, and configure output of gate to next item. KismetHelper.CreateOutputLink(newGate, "Out", newGate, 2); // Hook from Out to Close KismetHelper.CreateOutputLink(newGate, "Out", tubesP.GetUExport(1340), 0); // Hook from Out to Log (bypass the delay, we are repurposing it) MERFileSystem.SavePackage(tubesP); } }
private static void GenericRandomizeFlyerSpawns(IMEPackage package, int maxNumNewEnemies, EPortablePawnClassification minClassification = EPortablePawnClassification.Mook, EPortablePawnClassification maxClassification = EPortablePawnClassification.Boss) { var flyerPrepSequences = package.Exports.Where(x => x.ClassName == "Sequence" && x.GetProperty <StrProperty>("ObjName") is StrProperty objName && objName == "REF_SpawnPrep_Flyer").ToList(); foreach (var flySeq in flyerPrepSequences) { var objectsInSeq = KismetHelper.GetSequenceObjects(flySeq).OfType <ExportEntry>().ToList(); var preGate = objectsInSeq.FirstOrDefault(x => x.ClassName == "SeqAct_Gate"); // should not be null var outbound = SeqTools.GetOutboundLinksOfNode(preGate); var aifactoryObj = outbound[0][0].LinkedOp as ExportEntry; //First out on first link. Should point to AIFactory assuming these are all duplicated flyers var availablePawns = PawnPorting.PortablePawns.Where(x => x.Classification >= minClassification && x.Classification <= maxClassification).ToList(); if (availablePawns.Any()) { // We can add new pawns to install List <PortablePawn> newPawnsInThisSeq = new List <PortablePawn>(); int numEnemies = 1; // 1 is the original amount. List <ExportEntry> aiFactories = new List <ExportEntry>(); aiFactories.Add(aifactoryObj); // the original one for (int i = 0; i < maxNumNewEnemies; i++) { var randPawn = availablePawns.RandomElement(); if (!newPawnsInThisSeq.Contains(randPawn)) { numEnemies++; PawnPorting.PortPawnIntoPackage(randPawn, package); newPawnsInThisSeq.Add(randPawn); // Clone the ai factory sequence object and add it to the sequence var newAiFactorySeqObj = EntryCloner.CloneTree(aifactoryObj); aiFactories.Add(newAiFactorySeqObj); KismetHelper.AddObjectToSequence(newAiFactorySeqObj, flySeq, false); // Update the backing factory object var backingFactory = newAiFactorySeqObj.GetProperty <ObjectProperty>("Factory").ResolveToEntry(package) as ExportEntry; backingFactory.WriteProperty(new ObjectProperty(package.FindExport(randPawn.ChallengeTypeFullPath), "ActorType")); var collection = backingFactory.GetProperty <ArrayProperty <ObjectProperty> >("ActorResourceCollection"); collection.Clear(); foreach (var asset in randPawn.AssetPaths) { collection.Add(new ObjectProperty(package.FindExport(asset))); } backingFactory.WriteProperty(collection); } } if (newPawnsInThisSeq.Any()) { // install the switch var randSw = MERSeqTools.InstallRandomSwitchIntoSequence(flySeq, numEnemies); outbound[0][0].LinkedOp = randSw; SeqTools.WriteOutboundLinksToNode(preGate, outbound); // Hook up switch to ai factories for (int i = 0; i < numEnemies; i++) { var linkDesc = $"Link {i + 1}"; KismetHelper.CreateOutputLink(randSw, linkDesc, aiFactories[i]); // switches are indexed at 1 } } } } }
public static bool RandomizeBasicGestures(ExportEntry export, RandomizationOption option) { if (!CanRandomize(export)) { return(false); } if (export.GetProperty <ObjectProperty>("SkeletalMeshComponent")?.ResolveToEntry(export.FileRef) is ExportEntry smc) { //Debug.WriteLine($"Installing new lite animations for {export.InstancedFullPath}"); var animsets = smc.GetProperty <ArrayProperty <ObjectProperty> >("AnimSets"); var animTreeTemplate = smc.GetProperty <ObjectProperty>("AnimTreeTemplate")?.ResolveToEntry(export.FileRef) as ExportEntry; if (animsets != null && animTreeTemplate != null) { int numAnimationsSupported = 0; foreach (var animsetO in animsets) { var animset = animsetO.ResolveToEntry(export.FileRef) as ExportEntry; var sequences = animset.GetProperty <ArrayProperty <ObjectProperty> >("Sequences"); numAnimationsSupported += sequences.Count; } smc.RemoveProperty("AnimSets"); // We want to force new animations. we'll waste a bit of memory doing this but oh well List <RBioEvtSysTrackGesture.Gesture> installedGestures = new List <RBioEvtSysTrackGesture.Gesture>(); var animationPackagesCache = new MERPackageCache(); while (numAnimationsSupported > 0) { // should we make sure they're unique? var randGest = RBioEvtSysTrackGesture.InstallRandomFilteredGestureAsset(export.FileRef, 2, smaKeywords, null, null, true); InstallDynamicAnimSetRefForSkeletalMesh(smc, randGest); installedGestures.Add(randGest); numAnimationsSupported--; } animationPackagesCache.ReleasePackages(); var isSubfile = PackageTools.IsLevelSubfile(Path.GetFileName(export.FileRef.FilePath)); if (isSubfile) { var newName = export.ObjectName + "_MER"; export.ObjectName = new NameReference(newName, ThreadSafeRandom.Next(25685462)); } // Update the anim tree to use the new animations // Too lazy to properly trace to find nodes. Just take children of this node that are AnimNodeSequences // Add blend times to nodes so they 'blend' together a bit more, look a bit less jank SetupChildrenBlend(animTreeTemplate); // If the animtree has 'DebugPostLoad' flag, it means MER already is using this for something else // We need to generate a new tree so the animations work properly if (animTreeTemplate.ObjectFlags.Has(UnrealFlags.EObjectFlags.DebugPostLoad)) { animTreeTemplate = EntryCloner.CloneTree(animTreeTemplate, true); animTreeTemplate.ObjectName = export.FileRef.GetNextIndexedName("MER_AnimTree"); // New name smc.WriteProperty(new ObjectProperty(animTreeTemplate, "AnimTreeTemplate")); // Write the template back } else if (isSubfile) { // if it's a subfile it won't be used as an import // Let's rename this object animTreeTemplate.ObjectName = new NameReference("MER_AnimTree", ThreadSafeRandom.Next(200000000)); // New name } var animNodeSequences = export.FileRef.Exports.Where(x => x.idxLink == animTreeTemplate.UIndex && x.IsA("AnimNodeSequence")).ToList(); for (int i = 0; i < installedGestures.Count; i++) { var installedG = installedGestures[i]; var ans = animNodeSequences[i]; ans.WriteProperty(new NameProperty(installedG.GestureAnim, "AnimSeqName")); } animTreeTemplate.ObjectFlags |= UnrealFlags.EObjectFlags.DebugPostLoad; // Set as used return(true); } } return(false); }
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); } } }
private static void RandomizeAfterlifeShepDance() { var denDanceF = MERFileSystem.GetPackageFile(@"BioD_OmgHub_230DenDance.pcc"); if (denDanceF != null) { var loungeP = MEPackageHandler.OpenMEPackage(denDanceF); var sequence = loungeP.GetUExport(3924); MERPackageCache cache = new MERPackageCache(); List <InterpTools.InterpData> interpDatas = new List <InterpTools.InterpData>(); var interp1 = loungeP.GetUExport(3813); // Make 2 additional dance options by cloning the interp and the data tree var interp2 = MERSeqTools.CloneBasicSequenceObject(interp1); var interp3 = MERSeqTools.CloneBasicSequenceObject(interp1); // Clone the interp data for attaching to 2 and 3 var interpData1 = loungeP.GetUExport(1174); var interpData2 = EntryCloner.CloneTree(interpData1); var interpData3 = EntryCloner.CloneTree(interpData2); KismetHelper.AddObjectToSequence(interpData2, sequence); KismetHelper.AddObjectToSequence(interpData3, sequence); // Load ID for randomization interpDatas.Add(new InterpTools.InterpData(interpData1)); interpDatas.Add(new InterpTools.InterpData(interpData2)); interpDatas.Add(new InterpTools.InterpData(interpData3)); // Chance the data for interp2/3 var id2 = SeqTools.GetVariableLinksOfNode(interp2); id2[0].LinkedNodes[0] = interpData2; SeqTools.WriteVariableLinksToNode(interp2, id2); var id3 = SeqTools.GetVariableLinksOfNode(interp3); id3[0].LinkedNodes[0] = interpData3; SeqTools.WriteVariableLinksToNode(interp3, id3); // Add additional finished states for fadetoblack when done KismetHelper.CreateOutputLink(loungeP.GetUExport(958), "Finished", interp2, 2); KismetHelper.CreateOutputLink(loungeP.GetUExport(958), "Finished", interp3, 2); // Link up the random choice it makes var randSw = MERSeqTools.InstallRandomSwitchIntoSequence(sequence, 3); KismetHelper.CreateOutputLink(randSw, "Link 1", interp1); KismetHelper.CreateOutputLink(randSw, "Link 2", interp2); KismetHelper.CreateOutputLink(randSw, "Link 3", interp3); // Break the original output to start the interp, repoint it's output to the switch instead var sgm = loungeP.GetUExport(1003); //set gesture mode KismetHelper.RemoveOutputLinks(sgm); KismetHelper.CreateOutputLink(sgm, "Done", loungeP.GetUExport(960)); KismetHelper.CreateOutputLink(sgm, "Done", randSw); // Now install the dances foreach (var id in interpDatas) { var danceTrack = id.InterpGroups[0].Tracks[0]; OmegaHub.InstallShepardDanceGesture(danceTrack.Export, cache); } MERFileSystem.SavePackage(loungeP); } }
public static void RunGame2EmailMerge(GameTarget target) { M3MergeDLC.RemoveMergeDLC(target); var loadedFiles = MELoadedFiles.GetFilesLoadedInGame(target.Game, gameRootOverride: target.TargetPath); // File to base modifications on using IMEPackage pcc = MEPackageHandler.OpenMEPackage(loadedFiles[@"BioD_Nor103_Messages.pcc"]); // Path to Message templates file - different files for ME2/LE2 string ResourcesFilePath = $@"MassEffectModManagerCore.modmanager.emailmerge.{target.Game}.103Message_Template_{target.Game}"; using IMEPackage resources = MEPackageHandler.OpenMEPackageFromStream(Utilities.GetResourceStream(ResourcesFilePath)); // Startup file to place conditionals and transitions into using IMEPackage startup = MEPackageHandler.OpenMEPackageFromStream(Utilities.GetResourceStream( $@"MassEffectModManagerCore.modmanager.mergedlc.{target.Game}.Startup_{M3MergeDLC.MERGE_DLC_FOLDERNAME}.pcc")); var emailInfos = new List <ME2EmailMergeFile>(); var jsonSupercedances = M3Directories.GetFileSupercedances(target, new[] { @".json" }); if (jsonSupercedances.TryGetValue(EMAIL_MERGE_MANIFEST_FILE, out var jsonList)) { jsonList.Reverse(); foreach (var dlc in jsonList) { var jsonFile = Path.Combine(M3Directories.GetDLCPath(target), dlc, target.Game.CookedDirName(), EMAIL_MERGE_MANIFEST_FILE); emailInfos.Add(JsonConvert.DeserializeObject <ME2EmailMergeFile>(File.ReadAllText(jsonFile))); } } // Sanity checks if (!emailInfos.Any() || !emailInfos.SelectMany(e => e.Emails).Any()) { return; } if (emailInfos.Any(e => e.Game != target.Game)) { throw new Exception("ME2 email merge manifest targets incorrect game"); } // Startup File // Could replace this with full instanced path in M3 implementation ExportEntry stateEventMapExport = startup.Exports .First(e => e.ClassName == "BioStateEventMap" && e.ObjectName == "StateTransitionMap"); BioStateEventMap StateEventMap = stateEventMapExport.GetBinaryData <BioStateEventMap>(); ExportEntry ConditionalClass = startup.FindExport($@"PlotManager{M3MergeDLC.MERGE_DLC_FOLDERNAME}.BioAutoConditionals"); #region Sequence Exports // Send message - All email conditionals are checked and emails transitions are triggered ExportEntry SendMessageContainer = pcc.FindExport(@"TheWorld.PersistentLevel.Main_Sequence.Send_Messages"); ExportEntry LastSendMessage = KismetHelper.GetSequenceObjects(SendMessageContainer).OfType <ExportEntry>() .FirstOrDefault(e => { var outbound = KismetHelper.GetOutboundLinksOfNode(e); return(outbound.Count == 1 && outbound[0].Count == 0); }); ExportEntry TemplateSendMessage = resources.FindExport(@"TheWorld.PersistentLevel.Main_Sequence.Send_MessageTemplate"); ExportEntry TemplateSendMessageBoolCheck = resources.FindExport(@"TheWorld.PersistentLevel.Main_Sequence.Send_MessageTemplate_BoolCheck"); // Mark Read - email ints are set to read // This is the only section that does not gracefully handle different DLC installations - DLC_CER is required atm ExportEntry MarkReadContainer = pcc.FindExport(@"TheWorld.PersistentLevel.Main_Sequence.Mark_Read"); ExportEntry LastMarkRead = pcc.FindExport(@"TheWorld.PersistentLevel.Main_Sequence.Mark_Read.DLC_CER"); ExportEntry MarkReadOutLink = KismetHelper.GetOutboundLinksOfNode(LastMarkRead)[0][0].LinkedOp as ExportEntry; KismetHelper.RemoveOutputLinks(LastMarkRead); ExportEntry TemplateMarkRead = resources.FindExport(@"TheWorld.PersistentLevel.Main_Sequence.Mark_ReadTemplate"); ExportEntry TemplateMarkReadTransition = resources.FindExport(@"TheWorld.PersistentLevel.Main_Sequence.Mark_Read_Transition"); // Display Messages - Str refs are passed through to GUI ExportEntry DisplayMessageContainer = pcc.FindExport(@"TheWorld.PersistentLevel.Main_Sequence.Display_Messages"); ExportEntry DisplayMessageOutLink = pcc.FindExport(@"TheWorld.PersistentLevel.Main_Sequence.Display_Messages.SeqCond_CompareBool_0"); ExportEntry LastDisplayMessage = SeqTools.FindOutboundConnectionsToNode(DisplayMessageOutLink, KismetHelper.GetSequenceObjects(DisplayMessageContainer).OfType <ExportEntry>())[0]; KismetHelper.RemoveOutputLinks(LastDisplayMessage); var DisplayMessageVariableLinks = LastDisplayMessage.GetProperty <ArrayProperty <StructProperty> >("VariableLinks"); ExportEntry TemplateDisplayMessage = resources.FindExport(@"TheWorld.PersistentLevel.Main_Sequence.Display_MessageTemplate"); // Archive Messages - Message ints are set to 3 ExportEntry ArchiveContainer = pcc.FindExport(@"TheWorld.PersistentLevel.Main_Sequence.Archive_Message"); ExportEntry ArchiveSwitch = pcc.FindExport(@"TheWorld.PersistentLevel.Main_Sequence.Archive_Message.SeqAct_Switch_0"); ExportEntry ArchiveOutLink = pcc.FindExport( @"TheWorld.PersistentLevel.Main_Sequence.Archive_Message.BioSeqAct_PMCheckConditional_1"); ExportEntry ExampleSetInt = KismetHelper.GetOutboundLinksOfNode(ArchiveSwitch)[0][0].LinkedOp as ExportEntry; ExportEntry ExamplePlotInt = SeqTools.GetVariableLinksOfNode(ExampleSetInt)[0].LinkedNodes[0] as ExportEntry; #endregion int messageID = KismetHelper.GetOutboundLinksOfNode(ArchiveSwitch).Count + 1; int currentSwCount = ArchiveSwitch.GetProperty <IntProperty>("LinkCount").Value; foreach (var emailMod in emailInfos) { string modName = "DLC_MOD_" + emailMod.ModName; foreach (var email in emailMod.Emails) { string emailName = modName + "_" + email.EmailName; // Create send transition int transitionId = WriteTransition(StateEventMap, email.StatusPlotInt); int conditionalId = WriteConditional(email.TriggerConditional); #region SendMessage ////////////// // SendMessage ////////////// // Create seq object var SMTemp = emailMod.InMemoryBool.HasValue ? TemplateSendMessageBoolCheck : TemplateSendMessage; EntryImporter.ImportAndRelinkEntries(EntryImporter.PortingOption.CloneTreeAsChild, SMTemp, pcc, SendMessageContainer, true, new RelinkerOptionsPackage(), out var outSendEntry); var newSend = outSendEntry as ExportEntry; // Set name, comment, add to sequence newSend.ObjectName = new NameReference(emailName); KismetHelper.AddObjectToSequence(newSend, SendMessageContainer); KismetHelper.SetComment(newSend, emailName); if (target.Game == MEGame.ME2) { newSend.WriteProperty(new StrProperty(emailName, "ObjName")); } // Set Trigger Conditional var pmCheckConditionalSM = newSend.GetChildren() .FirstOrDefault(e => e.ClassName == "BioSeqAct_PMCheckConditional" && e is ExportEntry); if (pmCheckConditionalSM is ExportEntry conditional) { conditional.WriteProperty(new IntProperty(conditionalId, "m_nIndex")); KismetHelper.SetComment(conditional, "Time for " + email.EmailName + "?"); } // Set Send Transition var pmExecuteTransitionSM = newSend.GetChildren() .FirstOrDefault(e => e.ClassName == "BioSeqAct_PMExecuteTransition" && e is ExportEntry); if (pmExecuteTransitionSM is ExportEntry transition) { transition.WriteProperty(new IntProperty(transitionId, "m_nIndex")); KismetHelper.SetComment(transition, "Send " + email.EmailName + " message."); } // Set Send Transition if (emailMod.InMemoryBool.HasValue) { var pmCheckStateSM = newSend.GetChildren() .FirstOrDefault(e => e.ClassName == "BioSeqAct_PMCheckState" && e is ExportEntry); if (pmCheckStateSM is ExportEntry checkState) { checkState.WriteProperty(new IntProperty(emailMod.InMemoryBool.Value, "m_nIndex")); KismetHelper.SetComment(checkState, "Is " + emailMod.ModName + " installed?"); } } // Hook up output links KismetHelper.CreateOutputLink(LastSendMessage, "Out", newSend); LastSendMessage = newSend; #endregion #region MarkRead /////////// // MarkRead /////////// // Create seq object var MRTemp = email.ReadTransition.HasValue ? TemplateMarkReadTransition : TemplateMarkRead; EntryImporter.ImportAndRelinkEntries(EntryImporter.PortingOption.CloneTreeAsChild, MRTemp, pcc, MarkReadContainer, true, new RelinkerOptionsPackage(), out var outMarkReadEntry); var newMarkRead = outMarkReadEntry as ExportEntry; // Set name, comment, add to sequence newMarkRead.ObjectName = new NameReference(emailName); KismetHelper.AddObjectToSequence(newMarkRead, MarkReadContainer); KismetHelper.SetComment(newMarkRead, emailName); if (target.Game == MEGame.ME2) { newMarkRead.WriteProperty(new StrProperty(emailName, "ObjName")); } // Set Plot Int var storyManagerIntMR = newMarkRead.GetChildren() .FirstOrDefault(e => e.ClassName == "BioSeqVar_StoryManagerInt" && e is ExportEntry); if (storyManagerIntMR is ExportEntry plotIntMR) { plotIntMR.WriteProperty(new IntProperty(email.StatusPlotInt, "m_nIndex")); KismetHelper.SetComment(plotIntMR, email.EmailName); } if (email.ReadTransition.HasValue) { var pmExecuteTransitionMR = newMarkRead.GetChildren() .FirstOrDefault(e => e.ClassName == "BioSeqAct_PMExecuteTransition" && e is ExportEntry); if (pmExecuteTransitionMR is ExportEntry transitionMR) { transitionMR.WriteProperty(new IntProperty(email.ReadTransition.Value, "m_nIndex")); KismetHelper.SetComment(transitionMR, "Trigger " + email.EmailName + " read transition"); } } // Hook up output links KismetHelper.CreateOutputLink(LastMarkRead, "Out", newMarkRead); LastMarkRead = newMarkRead; #endregion #region DisplayEmail //////////////// // Display Email //////////////// // Create seq object EntryImporter.ImportAndRelinkEntries(EntryImporter.PortingOption.CloneTreeAsChild, TemplateDisplayMessage, pcc, DisplayMessageContainer, true, new RelinkerOptionsPackage(), out var outDisplayMessage); var newDisplayMessage = outDisplayMessage as ExportEntry; // Set name, comment, variable links, add to sequence newDisplayMessage.ObjectName = new NameReference(emailName); KismetHelper.AddObjectToSequence(newDisplayMessage, DisplayMessageContainer); newDisplayMessage.WriteProperty(DisplayMessageVariableLinks); KismetHelper.SetComment(newDisplayMessage, emailName); if (target.Game == MEGame.ME2) { newDisplayMessage.WriteProperty(new StrProperty(emailName, "ObjName")); } var displayChildren = newDisplayMessage.GetChildren(); // Set Plot Int var storyManagerIntDE = displayChildren.FirstOrDefault(e => e.ClassName == "BioSeqVar_StoryManagerInt" && e is ExportEntry); if (storyManagerIntDE is ExportEntry plotIntDE) { plotIntDE.WriteProperty(new IntProperty(email.StatusPlotInt, "m_nIndex")); } // Set Email ID var emailIdDE = displayChildren.FirstOrDefault(e => e.ClassName == "SeqVar_Int" && e is ExportEntry); if (emailIdDE is ExportEntry EmailIDDE) { EmailIDDE.WriteProperty(new IntProperty(messageID, "IntValue")); } // Set Title StrRef var titleStrRef = displayChildren.FirstOrDefault(e => e.ClassName == "BioSeqVar_StrRef" && e is ExportEntry ee && ee.GetProperty <NameProperty>("VarName").Value == "Title StrRef"); if (titleStrRef is ExportEntry Title) { Title.WriteProperty(new StringRefProperty(email.TitleStrRef, "m_srValue")); } // Set Description StrRef var descStrRef = displayChildren.FirstOrDefault(e => e.ClassName == "BioSeqVar_StrRef" && e is ExportEntry ee && ee.GetProperty <NameProperty>("VarName").Value == "Desc StrRef"); if (descStrRef is ExportEntry Desc) { Desc.WriteProperty(new StringRefProperty(email.DescStrRef, "m_srValue")); } // Hook up output links KismetHelper.CreateOutputLink(LastDisplayMessage, "Out", newDisplayMessage); LastDisplayMessage = newDisplayMessage; #endregion #region ArchiveEmail //////////////// // Archive Email //////////////// var NewSetInt = EntryCloner.CloneEntry(ExampleSetInt); KismetHelper.AddObjectToSequence(NewSetInt, ArchiveContainer); KismetHelper.CreateOutputLink(NewSetInt, "Out", ArchiveOutLink); KismetHelper.CreateNewOutputLink(ArchiveSwitch, "Link " + (messageID - 1), NewSetInt); var NewPlotInt = EntryCloner.CloneEntry(ExamplePlotInt); KismetHelper.AddObjectToSequence(NewPlotInt, ArchiveContainer); NewPlotInt.WriteProperty(new IntProperty(email.StatusPlotInt, "m_nIndex")); NewPlotInt.WriteProperty(new StrProperty(emailName, "m_sRefName")); var linkedVars = SeqTools.GetVariableLinksOfNode(NewSetInt); linkedVars[0].LinkedNodes = new List <IEntry>() { NewPlotInt }; SeqTools.WriteVariableLinksToNode(NewSetInt, linkedVars); messageID++; currentSwCount++; #endregion } } KismetHelper.CreateOutputLink(LastMarkRead, "Out", MarkReadOutLink); KismetHelper.CreateOutputLink(LastDisplayMessage, "Out", DisplayMessageOutLink); ArchiveSwitch.WriteProperty(new IntProperty(currentSwCount, "LinkCount")); stateEventMapExport.WriteBinary(StateEventMap); // Save Messages file into DLC var cookedDir = Path.Combine(M3Directories.GetDLCPath(target), M3MergeDLC.MERGE_DLC_FOLDERNAME, target.Game.CookedDirName()); var outMessages = Path.Combine(cookedDir, @"BioD_Nor103_Messages.pcc"); pcc.Save(outMessages); // Save Startup file into DLC var startupF = Path.Combine(cookedDir, $@"Startup_{M3MergeDLC.MERGE_DLC_FOLDERNAME}.pcc"); startup.Save(startupF); }
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); }