using HarmonyLib; using System.Globalization; using System.Linq; using System.Reflection; using System.Xml; using System.Xml.Linq; using UAI; using UnityEngine; public class UtilityAIPatches { private static readonly string AdvFeatureClass = "AdvancedTroubleshootingFeatures"; private static readonly string Feature = "UtilityAILogging"; private static readonly bool LoggingEnabled = AdvLogging.LogEnabled(AdvFeatureClass, Feature); private const BindingFlags _NonPublicStaticFlags = BindingFlags.Static | BindingFlags.NonPublic; /// /// Helper method to get the highest possible score for a package. /// The package's highest possible score is the highest weight out of any of its actions, /// multiplied by the package's weight. /// /// /// public static float GetHighestPossibleScore(UAIPackage package) { if (package == null) return 0f; var actions = package.GetActions(); if (actions == null || actions.Count == 0) return 0f; return package.Weight * actions.Max(a => a.Weight); } [HarmonyPatch(typeof(UAIFromXml))] [HarmonyPatch("parseAIPackagesNode")] public class UAIFromXml_parseAIPackagesNode { /// /// This postfix method allows modders to specify the maximum numbers of /// entities and/or waypoints that should be considered for actions. /// In this implementation, the specified maximums cannot go below the existing maximums. /// (Should this be changed?) /// /// /// Set the maximum entities to consider to 30, and the maximum waypoints to consider /// to 10. /// /// <ai_packages max_entities="30" max_waypoints="10> /// /// /// static void Postfix(XElement _element) { if (_element.HasAttribute("max_entities")) { int maxEntitiesToConsider = StringParsers.ParseSInt32( _element.GetAttribute("max_entities"), 0, -1, NumberStyles.Integer); UAIBase.MaxEntitiesToConsider = Utils.FastMax( UAIBase.MaxEntitiesToConsider, maxEntitiesToConsider); } if (_element.HasAttribute("max_waypoints")) { int maxWaypointsToConsider = StringParsers.ParseSInt32( _element.GetAttribute("max_waypoints"), 0, -1, NumberStyles.Integer); UAIBase.MaxWaypointsToConsider = Utils.FastMax( UAIBase.MaxWaypointsToConsider, maxWaypointsToConsider); } if (_element.HasAttribute("action_delay")) { UAIBase.ActionChoiceDelay = StringParsers.ParseFloat(_element.GetAttribute("action_delay")); } } } /* * sphereii notes: * This needs re-looked at, as it seems like the various packages were fighting constantly, and never sticking. * * ie: When a nurse was hired, and there was a Zombie Boe nearby, she would go back and forth between the Follow Leader and Move To Target tasks. */ //[HarmonyPatch(typeof(UAIBase))] //[HarmonyPatch("chooseAction")] //public class UAIBase_chooseAction //{ // /// // /// This prefix method replaces UAIBase.chooseAction in order to fix a bug, // /// and to introduce a "fail fast" mechanism for efficiency (hopefully). // /// // /// // /// // public static bool Prefix(Context _context) // { // float highScore = 0f; // _context.ConsiderationData.EntityTargets.Clear(); // _context.ConsiderationData.WaypointTargets.Clear(); // // These are private static methods so we need to call them using reflection // var addEntityTargetsToConsider = typeof(UAIBase).GetMethod( // "addEntityTargetsToConsider", // _NonPublicStaticFlags); // var addWaypointTargetsToConsider = typeof(UAIBase).GetMethod( // "addWaypointTargetsToConsider", // _NonPublicStaticFlags); // addEntityTargetsToConsider.Invoke(null, new object[] { _context }); // addWaypointTargetsToConsider.Invoke(null, new object[] { _context }); // // Sort AIPackages according to each package's highest possible score, descending. // // (If there is only one package, no sorting is needed.) // //if (_context.AIPackages.Count > 0) // //{ // // _context.AIPackages.Sort((a, b) => // // { // // if (!UAIBase.AIPackages.ContainsKey(a)) // // return 1; // a should go last // // if (!UAIBase.AIPackages.ContainsKey(b)) // // return -1; // b should go last // // var aScore = UtilityAIPatches.GetHighestPossibleScore(UAIBase.AIPackages[a]); // // var bScore = UtilityAIPatches.GetHighestPossibleScore(UAIBase.AIPackages[b]); // // return bScore.CompareTo(aScore); // // }); // //} // for (var i = 0; i < _context.AIPackages.Count; i++) // { // if (!UAIBase.AIPackages.ContainsKey(_context.AIPackages[i])) // continue; // var package = UAIBase.AIPackages[_context.AIPackages[i]]; // // If the current high score is greater than the highest score this package can // // produce, it's also higher than any of the remaining packages, so we're done. // // (If there is only one package, this test is not needed.) // if (i > 0 && highScore >= UtilityAIPatches.GetHighestPossibleScore(package)) // break; // UAIAction action; // object target; // var score = package.DecideAction(_context, out action, out target) * package.Weight; // // From vanilla: Only change the action if it's not already the action being taken. // // This also means the target will not change, even if the same action against a // // different target would produce a higher score this time. // // (Should we change this?) // if (score > highScore && _context.ActionData.Action != action) // { // if (_context.ActionData.Action != null && _context.ActionData.CurrentTask != null) // { // if (_context.ActionData.Started) // { // _context.ActionData.CurrentTask.Stop(_context); // } // if (_context.ActionData.Initialized) // { // _context.ActionData.CurrentTask.Reset(_context); // } // } // _context.ActionData.Action = action; // _context.ActionData.Target = target; // _context.ActionData.TaskIndex = 0; // highScore = score; // bug fix - vanilla code never sets the high score // } // } // // Don't call through to the original // return false; // } //} [HarmonyPatch(typeof(UAIAction))] [HarmonyPatch("GetScore")] public class UAIAction_GetScore { /// /// This prefix method fixes what I believe to be a bug in the vanilla code, having to do /// with a misplaced cast to float. It also changes the vanilla behavior, to not give a /// higher weight to actions that have more considerations. /// /// /// /// /// /// /// public static bool Prefix( // used by Harmony UAIAction __instance, ref float __result, // original parameters Context _context, object _target, float min = 0f) { if (__instance.GetTasks().Count == 0) { __result = 0f; return false; } var considerations = __instance.GetConsiderations(); if (considerations.Count == 0) { // The vanilla code returns score * this.Weight, but at this point, the score is // always 1, so we can save the extra multiplication and just use the weight. __result = __instance.Weight; return false; } if (LoggingEnabled) AdvLogging.DisplayLog(AdvFeatureClass, Feature, $"{_context.Self.EntityName} ( {_context.Self.entityId} ): {__instance.Name} Checking Considerations for Target: {_target}"); var score = 1f; for (var i = 0; i < considerations.Count; i++) { // bug fix: vanilla only fails fast if 0 > score and not if 0 == score //if (0f >= score || score < min) //{ // __result = 0f; // return false; //} /* * sphereii notes: * * Since we use a simpler way of failing a consideration, I've added in a quick fail here, which causes the task to fail out early. */ var consideration = considerations[i]; var considerationScore = consideration.GetScore(_context, _target); // This will fail the consideration if it returns zero before the response curve is // computed. NPC Core is depending upon this behavior, so do NOT change it! if (considerationScore == 0f) { if (LoggingEnabled) { AdvLogging.DisplayLog(AdvFeatureClass, Feature, $"\t{considerationScore} Score for {consideration.GetType()} Consideration Overall Score: {score}"); AdvLogging.DisplayLog(AdvFeatureClass, Feature, $"{__instance.Name} Task Failed due to above consideration"); } __result = 0f; return false; } considerationScore = consideration.ComputeResponseCurve(considerationScore); // Once the response curve is computed, we should also fail if it is zero, // since at that point the action's score can't be anything other than zero. if (considerationScore <= 0f) { if (LoggingEnabled) { AdvLogging.DisplayLog(AdvFeatureClass, Feature, $"\t{considerationScore} Score for {consideration.GetType()} Consideration Overall Score: {score}"); AdvLogging.DisplayLog(AdvFeatureClass, Feature, $"{__instance.Name} Task Failed due to above consideration"); } __result = 0f; return false; } score *= considerationScore; if (LoggingEnabled) AdvLogging.DisplayLog(AdvFeatureClass, Feature, $"\t{considerationScore} Score for {consideration.GetType()} Overall Score: {score}"); } // If we want to give a higher score to actions with more considerations, then // we can use a bugfixed version of the vanilla code: // __result = (score + (1f - score) * (1f - 1f / __instance.considerations.Count) * score) * __instance.Weight; __result = score * __instance.Weight; if (LoggingEnabled) AdvLogging.DisplayLog(AdvFeatureClass, Feature, $"{__result} Full Score for {__instance.GetType()} {_context.Self.EntityName} ( {_context.Self.entityId} )"); return false; } } }