internal static bool RandomizeExport(ExportEntry export, RandomizationOption option) { if (!CanRandomize(export)) { return(false); } #if DEBUG //if (!export.ObjectName.Name.Contains("HeavyWeaponMech")) // return false; #endif var powers = export.GetProperty <ArrayProperty <ObjectProperty> >("Powers"); if (powers == null) { // This loadout has no powers! // Randomly give them some powers. if (ThreadSafeRandom.Next(1) == 0) { // unlimited power List <ObjectProperty> blankPows = new List <ObjectProperty>(); // Add two blanks. We'll strip blanks before writing it blankPows.Add(new ObjectProperty(int.MinValue)); blankPows.Add(new ObjectProperty(int.MinValue)); powers = new ArrayProperty <ObjectProperty>(blankPows, "Powers"); } else { // Sorry mate no powers for you return(false); } } var originalPowerUIndexes = powers.Where(x => x.Value > 0).Select(x => x.Value).ToList(); foreach (var power in powers.ToList()) { if (power.Value == 0) { return(false); // Null entry in weapons list } IEntry existingPowerEntry = null; if (power.Value != int.MinValue) { // Husk AI kinda depends on melee or they just kinda breath on you all creepy like // We'll give them a chance to change it up though existingPowerEntry = power.ResolveToEntry(export.FileRef); if (existingPowerEntry.ObjectName.Name.Contains("Melee", StringComparison.InvariantCultureIgnoreCase) && ThreadSafeRandom.Next(2) == 0) { MERLog.Information($"Not changing melee power {existingPowerEntry.ObjectName.Name}"); continue; // Don't randomize power } if (PowersToNotSwap.Contains(existingPowerEntry.ObjectName.Name)) { MERLog.Information($"Not changing power {existingPowerEntry.ObjectName.Name}"); continue; // Do not change this power } } // DEBUG PowerInfo randomNewPower = Powers.RandomElement(); //if (option.SliderValue < 0) //{ // randomNewPower = Powers.RandomElement(); //} //else //{ // randomNewPower = Powers[(int)option.SliderValue]; //} // Prevent krogan from getting a death power while (export.ObjectName.Name.Contains("Krogan", StringComparison.InvariantCultureIgnoreCase) && randomNewPower.Type == EPowerCapabilityType.Death) { MERLog.Information(@"Re-roll no-death-power on krogan"); // Reroll. Krogan AI has something weird about it randomNewPower = Powers.RandomElement(); } // Prevent powerful enemies from getting super stat boosters while (randomNewPower.Type == EPowerCapabilityType.Buff && ( export.ObjectName.Name.Contains("Praetorian", StringComparison.InvariantCultureIgnoreCase) || export.ObjectName.Name.Contains("ShadowBroker", StringComparison.InvariantCultureIgnoreCase))) { MERLog.Information(@"Re-roll no-buffs for powerful enemy"); randomNewPower = Powers.RandomElement(); } #region YMIR MECH fixes if (export.ObjectName.Name.Contains("HeavyWeaponMech")) { // Heavy weapon mech chooses named death powers so we cannot change these // HeavyMechDeathExplosion is checked for existence. NormalExplosion for some reason isn't if ((existingPowerEntry.ObjectName.Name == "SFXPower_HeavyMechNormalExplosion")) { MERLog.Information($@"YMIR mech power HeavyMechNormalExplosion cannot be randomized, skipping"); continue; } // Do not add buff powers to YMIR while (randomNewPower.Type == EPowerCapabilityType.Buff) { MERLog.Information($@"Re-roll YMIR mech power to prevent potential enemy too difficult to kill softlock. Incompatible power: {randomNewPower.PowerName}"); randomNewPower = Powers.RandomElement(); } } #endregion // CHANGE THE POWER if (existingPowerEntry == null || randomNewPower.PowerName != existingPowerEntry.ObjectName) { if (powers.Any(x => power.Value != int.MinValue && power.ResolveToEntry(export.FileRef).ObjectName == randomNewPower.PowerName)) { continue; // Duplicate powers crash the game. It seems this code is not bulletproof here and needs changed a bit... } MERLog.Information($@"Changing power {export.ObjectName} {existingPowerEntry?.ObjectName ?? "(+New Power)"} => {randomNewPower.PowerName }"); // It's a different power. // See if we need to port this in var fullName = randomNewPower.PackageName + "." + randomNewPower.PowerName; // SFXGameContent_Powers.SFXPower_Hoops var existingVersionOfPower = export.FileRef.FindEntry(fullName); if (existingVersionOfPower != null) { // Power does not need ported in power.Value = existingVersionOfPower.UIndex; } else { // Power needs ported in power.Value = PortPowerIntoPackage(export.FileRef, randomNewPower, out _)?.UIndex ?? int.MinValue; } if (existingPowerEntry != null && existingPowerEntry.UIndex > 0 && PackageTools.IsPersistentPackage(export.FileRef.FilePath)) { // Make sure we add the original power to the list of referenced memory objects // so subfiles that depend on this power existing don't crash the game! var world = export.FileRef.FindExport("TheWorld"); var worldBin = ObjectBinary.From <World>(world); var extraRefs = worldBin.ExtraReferencedObjects.ToList(); extraRefs.Add(new UIndex(existingPowerEntry.UIndex)); worldBin.ExtraReferencedObjects = extraRefs.Distinct().ToArray(); // Filter out duplicates that may have already been in package world.WriteBinary(worldBin); } foreach (var addlPow in randomNewPower.AdditionalRequiredPowers) { var existingPow = export.FileRef.FindEntry(addlPow); //if (existingPow == null && randomNewPower.ImportOnly && sourcePackage != null) //{ // existingPow = PackageTools.CreateImportForClass(sourcePackage.FindExport(randomNewPower.PackageName + "." + randomNewPower.PowerName), export.FileRef); //} if (existingPow == null) { Debugger.Break(); } powers.Add(new ObjectProperty(existingPow)); } } } // Strip any blank powers we might have added, remove any duplicates powers.RemoveAll(x => x.Value == int.MinValue); powers.ReplaceAll(powers.ToList().Distinct()); //tolist prevents concurrent modification in nested linq // DEBUG #if DEBUG var duplicates = powers .GroupBy(i => i.Value) .Where(g => g.Count() > 1) .Select(g => g.Key).ToList(); if (duplicates.Any()) { foreach (var dup in duplicates) { Debug.WriteLine($"DUPLICATE POWER IN LOADOUT {export.FileRef.GetEntry(dup).ObjectName}"); } Debugger.Break(); } #endif export.WriteProperty(powers); // Our precalculated map should have accounted for imports already, so we odn't need to worry about missing imports upstream // If this is not a master or localization file (which are often used for imports) // Change the number around so it will work across packages. // May need disabled if game becomes unstable. // We check if less than 10 as it's very unlikely there will be more than 10 loadouts in a non-persistent package // if it's > 10 it's likely already a memory-changed item by MER var pName = Path.GetFileName(export.FileRef.FilePath); if (export.indexValue < 10 && !PackageTools.IsPersistentPackage(pName) && !PackageTools.IsLocalizationPackage(pName)) { export.ObjectName = new NameReference(export.ObjectName, ThreadSafeRandom.Next(2000)); } if (originalPowerUIndexes.Any()) { // We should ensure the original objects are still referenced so shared objects they have (vfx?) are kept in memory // Dunno if this actually fixes the problems... PackageTools.AddReferencesToWorld(export.FileRef, originalPowerUIndexes.Select(x => export.FileRef.GetUExport(x))); } return(true); }