Decompiled source of LooseLips v0.18.2
LooseLips.dll
Decompiled 2 days ago
The result has been truncated due to the large size, download it to view full contents!
using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Diagnostics; using System.Globalization; using System.IO; using System.Net.Http; using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.Versioning; using System.Security; using System.Security.Permissions; using System.Text; using System.Text.Json; using System.Text.Json.Serialization; using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; using BepInEx; using BepInEx.Configuration; using BepInEx.Core.Logging.Interpolation; using BepInEx.Logging; using BepInEx.Unity.IL2CPP; using HarmonyLib; using Il2CppInterop.Runtime.Injection; using Il2CppInterop.Runtime.InteropTypes; using Il2CppInterop.Runtime.InteropTypes.Arrays; using Il2CppSystem; using Il2CppSystem.Collections.Generic; using Il2CppSystem.Reflection; using LooseLips.Context; using LooseLips.Core; using LooseLips.Dialog; using LooseLips.Player2; using LooseLips.World; using Microsoft.CodeAnalysis; using UnityEngine; [assembly: CompilationRelaxations(8)] [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: Debuggable(DebuggableAttribute.DebuggingModes.Default | DebuggableAttribute.DebuggingModes.DisableOptimizations | DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints | DebuggableAttribute.DebuggingModes.EnableEditAndContinue)] [assembly: TargetFramework(".NETCoreApp,Version=v6.0", FrameworkDisplayName = ".NET 6.0")] [assembly: AssemblyCompany("LooseLips")] [assembly: AssemblyConfiguration("IL2CPP")] [assembly: AssemblyDescription("Talk to anyone in Shadows of Doubt in your own words. A local AI answers, and what it says changes the world.")] [assembly: AssemblyFileVersion("0.18.0.0")] [assembly: AssemblyInformationalVersion("0.18.0+9bb66c7d459af5d8d7b67c68bfd3722b9c8e233e")] [assembly: AssemblyProduct("Loose Lips")] [assembly: AssemblyTitle("LooseLips")] [assembly: SecurityPermission(SecurityAction.RequestMinimum, SkipVerification = true)] [assembly: AssemblyVersion("0.18.0.0")] [module: UnverifiableCode] [module: RefSafetyRules(11)] namespace Microsoft.CodeAnalysis { [CompilerGenerated] [Embedded] internal sealed class EmbeddedAttribute : Attribute { } } namespace System.Runtime.CompilerServices { [CompilerGenerated] [Embedded] [AttributeUsage(AttributeTargets.Class | AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Event | AttributeTargets.Parameter | AttributeTargets.ReturnValue | AttributeTargets.GenericParameter, AllowMultiple = false, Inherited = false)] internal sealed class NullableAttribute : Attribute { public readonly byte[] NullableFlags; public NullableAttribute(byte P_0) { NullableFlags = new byte[1] { P_0 }; } public NullableAttribute(byte[] P_0) { NullableFlags = P_0; } } [CompilerGenerated] [Embedded] [AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Method | AttributeTargets.Interface | AttributeTargets.Delegate, AllowMultiple = false, Inherited = false)] internal sealed class NullableContextAttribute : Attribute { public readonly byte Flag; public NullableContextAttribute(byte P_0) { Flag = P_0; } } [CompilerGenerated] [Embedded] [AttributeUsage(AttributeTargets.Module, AllowMultiple = false, Inherited = false)] internal sealed class RefSafetyRulesAttribute : Attribute { public readonly int Version; public RefSafetyRulesAttribute(int P_0) { Version = P_0; } } } namespace LooseLips { [BepInPlugin("dev.hubert.looselips", "Loose Lips", "0.18.0")] [BepInDependency(/*Could not decode attribute arguments.*/)] public class Plugin : BasePlugin { public static Plugin Instance { get; private set; } public static ManualLogSource Log { get; private set; } public override void Load() { //IL_004c: Unknown result type (might be due to invalid IL or missing references) //IL_0052: Expected O, but got Unknown Instance = this; Log = ((BasePlugin)this).Log; ModConfig.Bind(((BasePlugin)this).Config); Log.LogInfo((object)"Loose Lips v0.18.0 loading..."); SessionLog.Initialise(); SessionLog.BeginSession("0.18.0"); Player2Client.Initialise(); Harmony val = new Harmony("dev.hubert.looselips"); val.PatchAll(); ChatOverlay.Install(); Log.LogInfo((object)("Loaded. Talking to Player2 at " + ModConfig.BaseUrl.Value + ".")); } public override bool Unload() { Player2Client.Shutdown(); ConversationMemory.Save(); ConversationMemory.Clear(); WorldMemory.Save(); WorldMemory.Clear(); FollowDirector.StopAll(); Allegiance.Clear(); Negotiation.Clear(); AmbientReactions.Clear(); RequestBudget.Reset(); Player2Status.Reset(); VanillaLineCapture.Clear(); return ((BasePlugin)this).Unload(); } } public static class SessionHooks { [HarmonyPatch(typeof(DialogController), "SeenOrHeardUnusual")] public static class DialogController_SeenOrHeardUnusual { public static void Postfix(Citizen saysTo, Actor saidBy, NewRoom roomRef) { try { AmbientReactions.NoticedSomething(saysTo, saidBy, roomRef); } catch (Exception ex) { Plugin.Log.LogWarning((object)("Reaction hook failed: " + ex.Message)); } } } [HarmonyPatch(typeof(Toolbox), "Start")] public static class Toolbox_Start { public static void Postfix() { try { ChatOverlay.Install(); DialogRegistry.BuildPresets(); } catch (Exception ex) { Plugin.Log.LogError((object)("Toolbox.Start hook failed: " + ex)); } } } } public static class MyPluginInfo { public const string PLUGIN_GUID = "dev.hubert.looselips"; public const string PLUGIN_NAME = "Loose Lips"; public const string PLUGIN_VERSION = "0.18.0"; } } namespace LooseLips.World { public static class Allegiance { public enum Stance { Hostile, Wary, Neutral, Friendly, Ally } private static readonly Dictionary<int, Stance> Declared = new Dictionary<int, Stance>(); private static float _nextDefendCheck; public static Stance Of(Citizen citizen) { if ((Object)(object)citizen == (Object)null) { return Stance.Neutral; } if (Declared.TryGetValue(((Human)citizen).humanID, out var value)) { return value; } try { Player instance = Player.Instance; if ((Object)(object)instance == (Object)null) { return Stance.Neutral; } Acquaintance val = default(Acquaintance); if (!((Human)citizen).FindAcquaintanceExists((Human)(object)instance, ref val) || val == null) { return Stance.Neutral; } if (val.like < 0.2f) { return Stance.Hostile; } if (val.like < 0.4f) { return Stance.Wary; } if (val.like > 0.75f) { return Stance.Friendly; } } catch { } return Stance.Neutral; } public static string Describe(Citizen citizen) { return Of(citizen) switch { Stance.Hostile => "You have decided you are against this investigator.", Stance.Wary => "You do not trust this investigator.", Stance.Friendly => "You are on good terms with this investigator.", Stance.Ally => "You have taken this investigator's side, and will back them up.", _ => null, }; } public static string SideWith(Citizen citizen) { if (!ModConfig.AllowAllegiance.Value) { return "taking sides is switched off"; } if ((Object)(object)citizen == (Object)null) { return "nobody to take a side"; } try { Player instance = Player.Instance; Acquaintance val = default(Acquaintance); if (!((Object)(object)instance != (Object)null) || !((Human)citizen).FindAcquaintanceExists((Human)(object)instance, ref val) || val == null) { return "you are still a stranger to them"; } if (val.like < ModConfig.AllyLikeThreshold.Value) { return "they do not like you nearly enough for that"; } } catch (Exception ex) { return "checking how they feel threw: " + ex.Message; } Declared[((Human)citizen).humanID] = Stance.Ally; WorldMemory.Save(); return null; } public static string TurnAgainst(Citizen citizen) { if (!ModConfig.AllowAllegiance.Value) { return "taking sides is switched off"; } if ((Object)(object)citizen == (Object)null) { return "nobody to turn"; } Declared[((Human)citizen).humanID] = Stance.Hostile; FollowDirector.Stop(citizen); WorldMemory.Save(); return null; } public static void ClearDeclared(Citizen citizen) { if ((Object)(object)citizen != (Object)null) { Declared.Remove(((Human)citizen).humanID); } } public static void Clear() { Declared.Clear(); } public static Dictionary<string, string> Export() { Dictionary<string, string> dictionary = new Dictionary<string, string>(); foreach (KeyValuePair<int, Stance> item in Declared) { dictionary[item.Key.ToString()] = item.Value.ToString(); } return dictionary; } public static void Restore(Dictionary<string, string> saved) { Declared.Clear(); if (saved == null) { return; } foreach (KeyValuePair<string, string> item in saved) { if (int.TryParse(item.Key, out var result)) { try { Declared[result] = (Stance)Enum.Parse(typeof(Stance), item.Value, ignoreCase: true); } catch { } } } } public static bool IsAlly(Citizen citizen) { return Of(citizen) == Stance.Ally; } public static void DefendPlayer() { if (!ModConfig.AllowAllegiance.Value || !ModConfig.AlliesDefendYou.Value || Time.time < _nextDefendCheck) { return; } _nextDefendCheck = Time.time + 0.25f; Player instance = Player.Instance; if ((Object)(object)instance == (Object)null) { return; } List<Citizen> list; try { list = Earshot.CitizensWhoCanHear((Actor)(object)instance, shouted: true); } catch { return; } Citizen val = null; foreach (Citizen item in list) { try { if (!((Object)(object)item == (Object)null) && !((Object)(object)((Actor)item).ai == (Object)null) && ((Actor)item).ai.inCombat) { Actor attackTarget = ((Actor)item).ai.attackTarget; if (!((Object)(object)attackTarget == (Object)null) && !(((Il2CppObjectBase)attackTarget).Pointer != ((Il2CppObjectBase)instance).Pointer)) { val = item; break; } } } catch { } } if ((Object)(object)val == (Object)null) { return; } foreach (Citizen item2 in list) { try { if (!((Object)(object)item2 == (Object)null) && !((Object)(object)((Actor)item2).ai == (Object)null) && ((Human)item2).humanID != ((Human)val).humanID && IsAlly(item2) && !((Actor)item2).ai.restrained && !((Actor)item2).ai.inCombat && !(((Actor)item2).ai.alertness > ModConfig.AllyNerveThreshold.Value)) { ((Actor)item2).ai.SetInCombat(true, false); ((Actor)item2).ai.StartAttack((Actor)(object)val); SessionLog.Note(((Human)item2).GetCitizenName() + " stepped in against " + ((Human)val).GetCitizenName() + "."); } } catch { } } } } public static class AmbientReactions { private sealed class Trigger { public Citizen Who; public string What; public VoiceLevel Suggested; } private sealed class Watched { public float Alertness; public bool InCombat; public bool Fleeing; public bool Bleeding; public bool Trespassing; } private static readonly Dictionary<int, Watched> LastSeen = new Dictionary<int, Watched>(); private static readonly HashSet<int> WasNear = new HashSet<int>(); private static string _playerHeld = ""; private static bool _playerArmed; private static bool _watchingPlayerStarted; private static readonly Queue<Trigger> Pending = new Queue<Trigger>(); private static float _nextPoll; public static string LastLine { get; private set; } = "None yet."; private static bool PlayerIsTrespassing() { try { Player instance = Player.Instance; if ((Object)(object)instance == (Object)null || (Object)(object)((Actor)instance).currentRoom == (Object)null) { return false; } int num = default(int); return ((Actor)instance).IsTrespassing(((Actor)instance).currentRoom, ref num, true); } catch { return false; } } public static void NoticedSomething(Citizen who, Actor about, NewRoom where) { if (!ModConfig.EnableAmbientLife.Value || (Object)(object)who == (Object)null) { return; } try { if (!CanBeHeardByPlayer(who)) { return; } string text = "You have just noticed something out of place"; if ((Object)(object)about != (Object)null) { string text2 = SafeName(about); if (!string.IsNullOrEmpty(text2)) { text = "You have just caught " + text2 + " doing something they should not be"; } } if ((Object)(object)where != (Object)null) { try { text = text + " in " + where.GetName(); } catch { } } Enqueue(new Trigger { Who = who, What = text + ".", Suggested = VoiceLevel.Normal }); } catch { } } public static void Tick() { if (!ModConfig.EnableAmbientLife.Value) { return; } if (Time.time >= _nextPoll) { _nextPoll = Time.time + 0.5f; try { Poll(); } catch { } try { WatchThePlayer(); } catch { } try { NoticeArrivals(); } catch { } } Drain(); } private static void Poll() { Player instance = Player.Instance; if ((Object)(object)instance == (Object)null) { return; } foreach (Citizen item in Earshot.CitizensWhoCanHear((Actor)(object)instance, shouted: false)) { if ((Object)(object)item == (Object)null || (Object)(object)((Actor)item).ai == (Object)null) { continue; } if (!LastSeen.TryGetValue(((Human)item).humanID, out var value)) { LastSeen[((Human)item).humanID] = new Watched { Alertness = ((Actor)item).ai.alertness, InCombat = ((Actor)item).ai.inCombat, Fleeing = ((Actor)item).ai.inFleeState }; continue; } float alertness = ((Actor)item).ai.alertness; if (!value.InCombat && ((Actor)item).ai.inCombat) { Enqueue(new Trigger { Who = item, What = "A fight has just broken out and you are in it.", Suggested = VoiceLevel.Shout }); } else if (!value.Fleeing && ((Actor)item).ai.inFleeState) { Enqueue(new Trigger { Who = item, What = "You have just decided to run.", Suggested = VoiceLevel.Shout }); } else if (!value.Bleeding && ((Human)item).bleeding > 0.01f) { Enqueue(new Trigger { Who = item, What = "You are bleeding, and it has only just registered.", Suggested = VoiceLevel.Shout }); } else if (value.Trespassing != PlayerIsTrespassing() && PlayerIsTrespassing()) { Enqueue(new Trigger { Who = item, What = "The investigator has just walked into somewhere they have no business being.", Suggested = VoiceLevel.Normal }); } else if (alertness - value.Alertness >= ModConfig.AlarmJumpToReact.Value) { Enqueue(new Trigger { Who = item, What = "Something has just badly frightened you.", Suggested = VoiceLevel.Shout }); } value.Alertness = alertness; value.InCombat = ((Actor)item).ai.inCombat; value.Fleeing = ((Actor)item).ai.inFleeState; try { value.Bleeding = ((Human)item).bleeding > 0.01f; } catch { } value.Trespassing = PlayerIsTrespassing(); } } private static void WatchThePlayer() { if (!ModConfig.ReactToWhatYouDo.Value) { return; } Player instance = Player.Instance; if ((Object)(object)instance == (Object)null) { return; } string text = ""; bool flag = false; try { Interactable val = ((Actor)instance).rightHandInteractable ?? ((Actor)instance).leftHandInteractable; if (val != null) { text = val.GetName(); try { flag = (SoCustomComparison)(object)val.preset != (SoCustomComparison)null && (SoCustomComparison)(object)val.preset.weapon != (SoCustomComparison)null; } catch { flag = false; } } } catch { return; } if (!_watchingPlayerStarted) { _watchingPlayerStarted = true; _playerHeld = text; _playerArmed = flag; } else { if (text == _playerHeld && flag == _playerArmed) { return; } bool playerArmed = _playerArmed; _playerHeld = text; _playerArmed = flag; Citizen val2 = NearestWatcher(); if (!((Object)(object)val2 == (Object)null)) { if (flag && !playerArmed) { Enqueue(new Trigger { Who = val2, What = "The investigator in front of you has just drawn a " + Describe(text) + ".", Suggested = VoiceLevel.Shout }); } else if (!flag && playerArmed) { Enqueue(new Trigger { Who = val2, What = "The investigator has just put their weapon away.", Suggested = VoiceLevel.Normal }); } else if (!string.IsNullOrEmpty(text)) { Enqueue(new Trigger { Who = val2, What = "The investigator has just taken out a " + Describe(text) + ".", Suggested = VoiceLevel.Whisper }); } } } } private static void NoticeArrivals() { //IL_00ce: Unknown result type (might be due to invalid IL or missing references) //IL_00d9: Unknown result type (might be due to invalid IL or missing references) if (!ModConfig.GreetYouFirst.Value) { return; } Player instance = Player.Instance; if ((Object)(object)instance == (Object)null) { return; } HashSet<int> hashSet = new HashSet<int>(); foreach (Citizen item in Earshot.CitizensWhoCanHear((Actor)(object)instance, shouted: false)) { if ((Object)(object)item == (Object)null || (Object)(object)((Actor)item).ai == (Object)null) { continue; } try { if (((Actor)item).isDead || ((Actor)item).isAsleep || ((Actor)item).isStunned || ((Actor)item).ai.inCombat || ((Actor)item).ai.inFleeState || Vector3.Distance(((Component)item).transform.position, ((Component)instance).transform.position) > ModConfig.GreetingDistance.Value) { continue; } goto IL_00fe; } catch { } continue; IL_00fe: hashSet.Add(((Human)item).humanID); if (!WasNear.Contains(((Human)item).humanID)) { string text = ReasonToGreet(item); if (text != null) { Enqueue(new Trigger { Who = item, What = text, Suggested = VoiceLevel.Normal }); } } } WasNear.Clear(); foreach (int item2 in hashSet) { WasNear.Add(item2); } } private static string ReasonToGreet(Citizen c) { try { Allegiance.Stance stance = Allegiance.Of(c); int num = ConversationMemory.TurnsWith(((Human)c).humanID); switch (stance) { case Allegiance.Stance.Ally: return "The investigator whose side you took has just walked up. Greet them as your own."; case Allegiance.Stance.Hostile: return "The investigator you decided you were against has just walked up. You are not pleased."; default: if (Negotiation.PendingFor(c) != null) { return "The investigator who still owes you money has just walked up."; } if (FollowDirector.IsFollowing(c)) { return null; } if (num > 0) { return "Someone you have spoken with before has just walked up. Say something to them, as somebody who remembers the last conversation."; } return null; } } catch { return null; } } private static string Describe(string item) { return string.IsNullOrWhiteSpace(item) ? "something" : item.ToLowerInvariant(); } private static Citizen NearestWatcher() { //IL_0086: Unknown result type (might be due to invalid IL or missing references) //IL_0091: Unknown result type (might be due to invalid IL or missing references) Player instance = Player.Instance; if ((Object)(object)instance == (Object)null) { return null; } Citizen result = null; float num = float.MaxValue; foreach (Citizen item in Earshot.CitizensWhoCanHear((Actor)(object)instance, shouted: false)) { if ((Object)(object)item == (Object)null || (Object)(object)((Actor)item).ai == (Object)null) { continue; } try { if (!((Actor)item).isAsleep && !((Actor)item).isDead) { float num2 = Vector3.Distance(((Component)item).transform.position, ((Component)instance).transform.position); if (num2 < num) { num = num2; result = item; } } } catch { } } return result; } private static void Enqueue(Trigger trigger) { if (!((Object)(object)trigger?.Who == (Object)null) && Pending.Count < 4) { Pending.Enqueue(trigger); } } private static void Drain() { if (Pending.Count != 0) { Trigger trigger = Pending.Peek(); if ((Object)(object)trigger?.Who == (Object)null) { Pending.Dequeue(); } else if (RequestBudget.TryTake(RequestBudget.Kind.Ambient, trigger.Who)) { Pending.Dequeue(); Generate(trigger); } } } private static void Generate(Trigger trigger) { string prompt; try { prompt = BuildPrompt(trigger); } catch { RequestBudget.Finished(RequestBudget.Kind.Ambient); return; } Citizen who = trigger.Who; VoiceLevel suggested = trigger.Suggested; Task.Run(async delegate { NpcReply reply = null; try { reply = await Player2Client.GenerateReplyAsync(prompt, null, "React now, in one line.").ConfigureAwait(continueOnCapturedContext: false); } catch { } NpcReply captured = reply; MainThread.Post(delegate { try { Speak(who, captured, suggested); } finally { RequestBudget.Finished(RequestBudget.Kind.Ambient); } }); }); } private static string BuildPrompt(Trigger trigger) { CitizenSnapshot citizenSnapshot = ContextBuilder.Build(trigger.Who, shouted: false, null); StringBuilder stringBuilder = new StringBuilder(); stringBuilder.AppendLine("You are " + citizenSnapshot.FullName + ", a citizen of a rain-soaked voxel noir city."); if (citizenSnapshot.Traits.Count > 0) { stringBuilder.AppendLine("You are " + string.Join(", ", citizenSnapshot.Traits) + "."); } if (!string.IsNullOrEmpty(citizenSnapshot.Job)) { stringBuilder.AppendLine("You work as " + citizenSnapshot.Job + "."); } stringBuilder.AppendLine(); stringBuilder.AppendLine("# What just happened"); stringBuilder.AppendLine(trigger.What); if (citizenSnapshot.Bystanders.Count > 0) { stringBuilder.AppendLine("Within earshot: " + string.Join(", ", citizenSnapshot.Bystanders) + "."); } else { stringBuilder.AppendLine("Nobody is close by."); } stringBuilder.AppendLine(); stringBuilder.AppendLine("# How to answer"); stringBuilder.AppendLine("Say one short line out loud, as this person, reacting to it. Reply with JSON only:"); stringBuilder.AppendLine("{ \"speech\": \"...\", \"voice\": \"whisper\" or \"normal\" or \"shout\" }"); stringBuilder.AppendLine("A moment like this is usually " + Voice.Describe(trigger.Suggested) + "."); stringBuilder.AppendLine("Follow that unless this particular person genuinely would not: fear, warnings and"); stringBuilder.AppendLine("anything meant to carry are shouted, remarks to somebody standing beside you are"); stringBuilder.AppendLine("whispered, everything else is normal."); stringBuilder.AppendLine("At most " + Mathf.Min(ModConfig.MaxReplyCharacters.Value, 120) + " characters. No stage directions."); return stringBuilder.ToString(); } private static void Speak(Citizen who, NpcReply reply, VoiceLevel suggested) { if (!((Object)(object)who == (Object)null) && reply != null && !string.IsNullOrWhiteSpace(reply.Speech) && CanBeHeardByPlayer(who)) { VoiceLevel level = Voice.Parse(reply.Voice, suggested); SpeechRelay.CitizenSaysAt(who, reply.Speech, level); LastLine = ((Human)who).GetCitizenName() + " (" + Voice.Describe(level) + "): " + reply.Speech; SessionLog.Note("Ambient - " + LastLine); } } private static bool CanBeHeardByPlayer(Citizen who) { //IL_0034: Unknown result type (might be due to invalid IL or missing references) //IL_003f: Unknown result type (might be due to invalid IL or missing references) try { Player instance = Player.Instance; if ((Object)(object)instance == (Object)null || (Object)(object)((who != null) ? ((Component)who).transform : null) == (Object)null) { return false; } return Vector3.Distance(((Component)who).transform.position, ((Component)instance).transform.position) <= ModConfig.ShoutRadius.Value; } catch { return false; } } private static string SafeName(Actor actor) { try { Citizen val = ((Il2CppObjectBase)actor).TryCast<Citizen>(); if ((Object)(object)val != (Object)null) { return ((Human)val).GetCitizenName(); } return actor.isPlayer ? "the investigator" : null; } catch { return null; } } public static void Clear() { LastSeen.Clear(); Pending.Clear(); _playerHeld = ""; _playerArmed = false; _watchingPlayerStarted = false; WasNear.Clear(); } } public static class BystanderReactions { public static void Propagate(Citizen speaker, NpcReply reply, bool shouted) { //IL_0186: Unknown result type (might be due to invalid IL or missing references) //IL_01f3: Unknown result type (might be due to invalid IL or missing references) if ((Object)(object)speaker == (Object)null || reply == null || !ModConfig.EnableWorldEffects.Value) { return; } float num = Mathf.Clamp01(reply.Alarm); if (!shouted && num < 0.4f) { return; } List<Citizen> list = Earshot.CitizensWhoCanHear((Actor)(object)speaker, shouted); if (list.Count == 0) { return; } float num2 = (shouted ? (num * 0.6f) : (num * 0.25f)); if (num2 < 0.02f) { return; } Player instance = Player.Instance; float value = ModConfig.MaxSuspicionShiftPerLine.Value; foreach (Citizen item in list) { if ((Object)(object)item == (Object)null || ((Human)item).humanID == ((Human)speaker).humanID) { continue; } try { if ((Object)(object)((Actor)item).ai == (Object)null) { continue; } float num3 = Mathf.Clamp(num2, 0f, value); ((Actor)item).ai.alertness = Mathf.Clamp01(((Actor)item).ai.alertness + num3); if (shouted && num >= 0.5f) { ((Actor)item).ai.TriggerReactionIndicator(); if ((Object)(object)instance != (Object)null && ((Actor)item).currentNode != null) { ((Actor)item).ai.SetFacingPosition(((Component)instance).transform.position); } } if (shouted && num >= 0.75f && ((Actor)item).isEnforcer && ((Actor)item).isOnDuty && ModConfig.AllowPoliceRedirection.Value && (Object)(object)instance != (Object)null && ((Actor)instance).currentNode != null) { ((Actor)item).ai.Investigate(((Actor)instance).currentNode, ((Component)instance).transform.position, (Actor)null, (ReactionState)2, 1f, 0, false, 1f, (Interactable)null); } } catch (Exception ex) { if (ModConfig.VerboseLogging.Value) { Plugin.Log.LogWarning((object)("Bystander reaction failed: " + ex.Message)); } } } if (ModConfig.VerboseLogging.Value) { Plugin.Log.LogInfo((object)("Overheard by " + list.Count + " citizen(s), spread " + num2.ToString("0.00"))); } } } public static class CrowdEffects { private static List<Citizen> Audience(Citizen speaker, bool shouted) { List<Citizen> list = new List<Citizen>(); foreach (Citizen item in Earshot.CitizensWhoCanHear((Actor)(object)speaker, shouted)) { if (!((Object)(object)item == (Object)null) && (!((Object)(object)speaker != (Object)null) || ((Human)item).humanID != ((Human)speaker).humanID)) { list.Add(item); } } return list; } public static string Panic(Citizen speaker, bool shouted) { if (!ModConfig.AllowCrowdEffects.Value) { return "crowd effects are switched off"; } if (!ModConfig.AllowCombatEffects.Value) { return "fleeing and combat are switched off"; } List<Citizen> list = Audience(speaker, shouted); if (list.Count == 0) { return "nobody else heard it"; } int num = 0; foreach (Citizen item in list) { try { if (!((Object)(object)((Actor)item).ai == (Object)null) && !((Actor)item).ai.restrained) { ((Actor)item).ai.CancelCombat(); ((Actor)item).ai.inFleeState = true; ((Actor)item).ai.TriggerReactionIndicator(); num++; } } catch { } } return (num > 0) ? null : "nobody in earshot could run"; } public static string Settle(Citizen speaker, bool shouted) { if (!ModConfig.AllowCrowdEffects.Value) { return "crowd effects are switched off"; } List<Citizen> list = Audience(speaker, shouted); if (list.Count == 0) { return "nobody else heard it"; } float value = ModConfig.MaxSuspicionShiftPerLine.Value; int num = 0; foreach (Citizen item in list) { try { if (!((Object)(object)((Actor)item).ai == (Object)null)) { ((Actor)item).ai.inFleeState = false; ((Actor)item).ai.alertness = Mathf.Clamp01(((Actor)item).ai.alertness - value); num++; } } catch { } } return (num > 0) ? null : "nobody in earshot could be calmed"; } public static string Gather(Citizen speaker, bool shouted) { if (!ModConfig.AllowCrowdEffects.Value) { return "crowd effects are switched off"; } if (!ModConfig.AllowGoalRedirection.Value) { return "changing what people are doing is switched off"; } List<Citizen> list = Audience(speaker, shouted); if (list.Count == 0) { return "nobody else heard it"; } int num = 0; foreach (Citizen item in list) { if (GoalDirector.InvestigateHere(item, shouted) == null) { num++; } } return (num > 0) ? null : "nobody in earshot could come and look"; } public static int Size(Citizen speaker, bool shouted) { return Audience(speaker, shouted).Count; } } public static class Disclosure { public static string Reveal(Citizen citizen, string what) { //IL_00c2: Unknown result type (might be due to invalid IL or missing references) //IL_00c6: Unknown result type (might be due to invalid IL or missing references) //IL_00cc: Unknown result type (might be due to invalid IL or missing references) //IL_00ee: Unknown result type (might be due to invalid IL or missing references) if (!ModConfig.AllowDisclosure.Value) { return "filing details is switched off"; } if ((Object)(object)citizen == (Object)null) { return "nobody to file"; } DataKey key; string text = Resolve(citizen, what, out key); if (text != null) { return text; } try { CasePanelController instance = CasePanelController.Instance; if ((Object)(object)instance == (Object)null) { return "the case panel is not up yet"; } Case activeCase = instance.activeCase; if (activeCase == null) { return "there is no case open to file it under"; } Interactable interactable = ((Actor)citizen).interactable; Evidence val = ((interactable != null) ? interactable.evidence : null); if (val == null) { return "the game keeps no evidence entry for them"; } instance.PinToCasePanel(activeCase, val, key, false, default(Vector2), false); SessionLog.Note(((Human)citizen).GetCitizenName() + " gave up their " + Describe(key) + ", filed under " + activeCase.name + "."); return null; } catch (Exception ex) { return "the game refused to file it: " + ex.Message; } } public static List<string> PossibleDetails(Citizen citizen) { List<string> list = new List<string>(); if ((Object)(object)citizen == (Object)null) { return list; } try { list.Add("name"); if ((Object)(object)((Human)citizen).home != (Object)null) { list.Add("address"); } if (((Human)citizen).job != null) { list.Add("job"); } if (((Human)citizen).job != null && ((Human)citizen).job.employer != null) { list.Add("workplace"); } if ((Object)(object)((Human)citizen).partner != (Object)null) { list.Add("partner"); } } catch (Exception ex) { if (ModConfig.VerboseLogging.Value) { Plugin.Log.LogWarning((object)("Could not list what they could disclose: " + ex.Message)); } } return list; } private static string Resolve(Citizen citizen, string what, out DataKey key) { key = (DataKey)0; string text = (what ?? string.Empty).Trim().ToLowerInvariant(); if (text.Length == 0) { return "they did not say which detail"; } try { if (text.Contains("name") && !text.Contains("partner")) { key = (DataKey)0; return null; } if (text.Contains("address") || text.Contains("home") || text.Contains("live")) { if ((Object)(object)((Human)citizen).home == (Object)null) { return "they have nowhere the game calls home"; } key = (DataKey)14; return null; } if (text.Contains("workplace") || text.Contains("employer")) { if (((Human)citizen).job == null || ((Human)citizen).job.employer == null) { return "they do not work anywhere"; } key = (DataKey)15; return null; } if (text.Contains("job") || text.Contains("work") || text.Contains("title")) { if (((Human)citizen).job == null) { return "they have no job"; } key = (DataKey)17; return null; } if (text.Contains("partner") || text.Contains("spouse") || text.Contains("married")) { if ((Object)(object)((Human)citizen).partner == (Object)null) { return "they have no partner"; } key = (DataKey)26; return null; } if (text.Contains("phone") || text.Contains("telephone") || text.Contains("number")) { key = (DataKey)33; return null; } } catch (Exception ex) { return "checking what they have threw: " + ex.Message; } return "there is no such detail to file"; } private static string Describe(DataKey key) { //IL_0001: Unknown result type (might be due to invalid IL or missing references) //IL_0002: Unknown result type (might be due to invalid IL or missing references) //IL_0003: Unknown result type (might be due to invalid IL or missing references) //IL_0004: Unknown result type (might be due to invalid IL or missing references) //IL_0005: Unknown result type (might be due to invalid IL or missing references) //IL_0008: Unknown result type (might be due to invalid IL or missing references) //IL_001e: Expected I4, but got Unknown //IL_0020: Unknown result type (might be due to invalid IL or missing references) //IL_0023: Invalid comparison between Unknown and I4 //IL_0027: Unknown result type (might be due to invalid IL or missing references) //IL_002a: Invalid comparison between Unknown and I4 switch (key - 14) { default: if ((int)key != 26) { if ((int)key != 33) { break; } return "telephone number"; } return "partner"; case 0: return "address"; case 1: return "workplace"; case 3: return "job"; case 2: break; } return "name"; } } public static class Disposition { private static readonly string[] Mercenary = new string[10] { "greed", "money", "miser", "cheap", "gambl", "debt", "poor", "broke", "business", "corrupt" }; private static readonly string[] Generous = new string[8] { "generous", "kind", "helpful", "honest", "friendly", "charit", "loyal", "polite" }; private static readonly string[] Timid = new string[7] { "timid", "nervous", "cowar", "anxious", "meek", "frail", "shy" }; private static readonly string[] Aggressive = new string[8] { "aggress", "hot", "violent", "brave", "brawl", "temper", "bully", "fearless" }; private static readonly string[] Talkative = new string[6] { "gossip", "nosy", "chatty", "talkative", "loud", "curious" }; private static bool Any(IEnumerable<string> traits, string[] keywords) { if (traits == null) { return false; } foreach (string trait in traits) { if (string.IsNullOrEmpty(trait)) { continue; } string text = trait.ToLowerInvariant(); foreach (string value in keywords) { if (text.Contains(value)) { return true; } } } return false; } public static bool WouldHaggle(CitizenSnapshot s) { if (s == null) { return true; } if (Any(s.Traits, Mercenary)) { return true; } if (Any(s.Traits, Generous)) { return false; } return s.Like < 0.45f; } public static bool WouldFight(CitizenSnapshot s) { if (s == null) { return true; } if (s.IsEnforcer || s.CitizenIsArmed) { return true; } if (Any(s.Traits, Aggressive)) { return true; } return !Any(s.Traits, Timid); } public static bool WouldFlee(CitizenSnapshot s) { if (s == null) { return true; } if (Any(s.Traits, Timid)) { return true; } return !s.IsEnforcer; } public static bool WouldTalkAboutOthers(CitizenSnapshot s) { if (s == null) { return true; } if (s.Opinions == null || s.Opinions.Count == 0) { return false; } if (Any(s.Traits, Talkative)) { return true; } return !Any(s.Traits, Generous) || s.Like > 0.6f; } public static string Describe(CitizenSnapshot s) { if (s == null) { return null; } List<string> list = new List<string>(); if (Any(s.Traits, Mercenary)) { list.Add("you do not do favours for nothing"); } if (Any(s.Traits, Generous)) { list.Add("you help people without being asked twice"); } if (Any(s.Traits, Timid)) { list.Add("you frighten easily"); } if (Any(s.Traits, Aggressive)) { list.Add("you do not back down"); } if (Any(s.Traits, Talkative)) { list.Add("you enjoy talking about other people"); } return (list.Count == 0) ? null : ("In character: " + string.Join(", ", list) + "."); } } public static class Earshot { private sealed class CachedSweep { public float TakenAt; public List<Citizen> Result; } private static readonly Dictionary<long, CachedSweep> Recent = new Dictionary<long, CachedSweep>(); private const float CacheSeconds = 0.1f; public static float Radius(bool shouted) { return shouted ? ModConfig.ShoutRadius.Value : ModConfig.TalkRadius.Value; } public static float Radius(VoiceLevel level) { return Voice.RadiusOf(level); } public static List<Citizen> CitizensWhoCanHear(Actor origin, VoiceLevel level) { return Gather(origin, Voice.RadiusOf(level), Voice.CarriesNextDoor(level)); } public static List<Citizen> CitizensWhoCanHear(Actor origin, bool shouted) { return Cached(origin, Radius(shouted), shouted); } private static List<Citizen> Cached(Actor origin, float radius, bool shouted) { if ((Object)(object)origin == (Object)null) { return new List<Citizen>(); } long key; try { key = ((long)((Object)origin).GetInstanceID() << 20) ^ (long)(radius * 100f); } catch { return Gather(origin, radius, shouted); } if (Recent.TryGetValue(key, out var value) && Time.time - value.TakenAt < 0.1f) { return value.Result; } List<Citizen> result = Gather(origin, radius, shouted); Recent[key] = new CachedSweep { TakenAt = Time.time, Result = result }; if (Recent.Count > 32) { Prune(); } return result; } private static void Prune() { List<long> list = new List<long>(); foreach (KeyValuePair<long, CachedSweep> item in Recent) { if (Time.time - item.Value.TakenAt >= 0.1f) { list.Add(item.Key); } } foreach (long item2 in list) { Recent.Remove(item2); } } public static void ClearCache() { Recent.Clear(); } private static List<Citizen> Gather(Actor origin, float radius, bool shouted) { //IL_0037: Unknown result type (might be due to invalid IL or missing references) //IL_002a: Unknown result type (might be due to invalid IL or missing references) //IL_003c: Unknown result type (might be due to invalid IL or missing references) //IL_0191: Unknown result type (might be due to invalid IL or missing references) //IL_025a: Unknown result type (might be due to invalid IL or missing references) List<Citizen> list = new List<Citizen>(); if ((Object)(object)origin == (Object)null) { return list; } Vector3 originPos = (((Object)(object)((Component)origin).transform != (Object)null) ? ((Component)origin).transform.position : Vector3.zero); HashSet<NewRoom> hashSet = new HashSet<NewRoom>(); try { if ((Object)(object)origin.currentRoom != (Object)null) { hashSet.Add(origin.currentRoom); if (shouted && origin.currentRoom.adjacentRooms != null) { Enumerator<NewRoom> enumerator = origin.currentRoom.adjacentRooms.GetEnumerator(); while (enumerator.MoveNext()) { NewRoom current = enumerator.Current; if ((Object)(object)current != (Object)null) { hashSet.Add(current); } } } } } catch (Exception ex) { if (ModConfig.VerboseLogging.Value) { Plugin.Log.LogWarning((object)("Room walk failed: " + ex.Message)); } } HashSet<int> hashSet2 = new HashSet<int>(); foreach (NewRoom item in hashSet) { HashSet<Actor> val = null; try { val = item.currentOccupants; } catch { } if (val == null) { continue; } Enumerator<Actor> enumerator3 = val.GetEnumerator(); while (enumerator3.MoveNext()) { Actor current3 = enumerator3.Current; Citizen val2 = TryAsCitizen(current3); if (!((Object)(object)val2 == (Object)null) && CanHear(val2) && hashSet2.Add(((Human)val2).humanID) && WithinRange((Actor)(object)val2, originPos, radius)) { list.Add(val2); } } } if (hashSet.Count == 0) { try { List<Citizen> val3 = (((Object)(object)CityData.Instance != (Object)null) ? CityData.Instance.citizenDirectory : null); if (val3 != null) { Enumerator<Citizen> enumerator4 = val3.GetEnumerator(); while (enumerator4.MoveNext()) { Citizen current4 = enumerator4.Current; if (!((Object)(object)current4 == (Object)null) && CanHear(current4) && hashSet2.Add(((Human)current4).humanID) && WithinRange((Actor)(object)current4, originPos, radius)) { list.Add(current4); } } } } catch (Exception ex2) { if (ModConfig.VerboseLogging.Value) { Plugin.Log.LogWarning((object)("Directory sweep failed: " + ex2.Message)); } } } return list; } private static bool WithinRange(Actor actor, Vector3 originPos, float radius) { //IL_001c: Unknown result type (might be due to invalid IL or missing references) //IL_0021: Unknown result type (might be due to invalid IL or missing references) try { if ((Object)(object)((Component)actor).transform == (Object)null) { return false; } return Vector3.Distance(((Component)actor).transform.position, originPos) <= radius; } catch { return false; } } private static bool CanHear(Citizen cit) { try { if (((Actor)cit).isPlayer || ((Actor)cit).isDead || ((Actor)cit).isAsleep || ((Actor)cit).isStunned) { return false; } return ((Actor)cit).canListen; } catch { return false; } } private static Citizen TryAsCitizen(Actor actor) { if ((Object)(object)actor == (Object)null) { return null; } try { return ((Il2CppObjectBase)actor).TryCast<Citizen>(); } catch { return null; } } } public static class EffectCatalogue { public sealed class Request { public Citizen Speaker; public string Target; public string Detail; public bool Shouted; public string PlayerLine; } public sealed class Definition { public string Name; public string Description; public Func<bool> Enabled = () => true; public Func<Request, string> Run; public string[] Aliases = new string[0]; public Func<CitizenSnapshot, bool> Relevant; public string Conflicts; public bool NeedsInvitation; public bool Offered => Enabled() && !string.IsNullOrEmpty(Description); public bool RelevantTo(CitizenSnapshot snapshot) { if (Relevant == null) { return true; } try { return Relevant(snapshot); } catch { return true; } } } private static readonly List<Definition> All = new List<Definition>(); private static readonly Dictionary<string, Definition> ByName = new Dictionary<string, Definition>(); public static int Count => All.Count; public static void Register(Definition definition) { if (definition == null || string.IsNullOrWhiteSpace(definition.Name) || definition.Run == null) { return; } All.Add(definition); ByName[Normalise(definition.Name)] = definition; string[] aliases = definition.Aliases; foreach (string text in aliases) { if (!string.IsNullOrWhiteSpace(text)) { ByName[Normalise(text)] = definition; } } } public static IEnumerable<Definition> Offered(CitizenSnapshot snapshot = null) { foreach (Definition definition in All) { if (definition.Offered && (snapshot == null || definition.RelevantTo(snapshot))) { yield return definition; } } } public static Definition Find(string written) { if (string.IsNullOrWhiteSpace(written)) { return null; } Definition value; return ByName.TryGetValue(Normalise(written), out value) ? value : null; } public static string Normalise(string s) { if (string.IsNullOrWhiteSpace(s)) { return ""; } List<char> list = new List<char>(s.Length); bool flag = false; string text = s.Trim(); foreach (char c in text) { char c2 = c; if (c2 == ' ' || c2 == '-' || c2 == '.' || c2 == '/') { if (list.Count > 0 && list[list.Count - 1] != '_') { list.Add('_'); } flag = false; } else if (char.IsUpper(c2)) { if (flag && list.Count > 0 && list[list.Count - 1] != '_') { list.Add('_'); } list.Add(char.ToLowerInvariant(c2)); flag = false; } else if (char.IsLetterOrDigit(c2) || c2 == '_') { list.Add(c2); flag = char.IsLower(c2); } } return new string(list.ToArray()).Trim('_'); } public static void Reset() { All.Clear(); ByName.Clear(); } } public static class FollowDirector { private sealed class Follower { public int Id; public float Until; public float NextNudge; } private static readonly Dictionary<int, Follower> Following = new Dictionary<int, Follower>(); public static int Count => Following.Count; private static Citizen Resolve(int id) { try { CityData instance = CityData.Instance; if ((Object)(object)instance == (Object)null) { return null; } Human val = default(Human); if (!instance.GetHuman(id, ref val, false) || (Object)(object)val == (Object)null) { return null; } return ((Il2CppObjectBase)val).TryCast<Citizen>(); } catch { return null; } } public static bool IsFollowing(Citizen c) { return (Object)(object)c != (Object)null && Following.ContainsKey(((Human)c).humanID); } public static List<string> Names() { List<string> list = new List<string>(); foreach (Follower value in Following.Values) { try { Citizen val = Resolve(value.Id); if ((Object)(object)val != (Object)null) { list.Add(((Human)val).GetCitizenName()); } } catch { } } return list; } public static string Start(Citizen citizen) { if (!ModConfig.AllowFollowing.Value) { return "getting people to follow you is switched off"; } if ((Object)(object)citizen == (Object)null || (Object)(object)((Actor)citizen).ai == (Object)null) { return "no AI on this citizen"; } if (((Actor)citizen).ai.restrained) { return "they are restrained"; } if (Following.Count >= ModConfig.MaxFollowers.Value && !IsFollowing(citizen)) { return "you already have as many people with you as the mod allows"; } Following[((Human)citizen).humanID] = new Follower { Id = ((Human)citizen).humanID, Until = Time.time + ModConfig.FollowDuration.Value, NextNudge = 0f }; return null; } public static string Stop(Citizen citizen) { if ((Object)(object)citizen == (Object)null) { return "nobody to stop"; } if (!Following.Remove(((Human)citizen).humanID)) { return "they were not following you"; } return null; } public static void StopAll() { Following.Clear(); } public static void Tick() { //IL_0199: Unknown result type (might be due to invalid IL or missing references) //IL_00d7: Unknown result type (might be due to invalid IL or missing references) //IL_00e2: Unknown result type (might be due to invalid IL or missing references) if (Following.Count == 0) { return; } Player instance = Player.Instance; if ((Object)(object)instance == (Object)null) { Following.Clear(); return; } List<int> list = null; foreach (KeyValuePair<int, Follower> item in Following) { Follower value = item.Value; Citizen val = Resolve(value.Id); bool flag = false; try { if ((Object)(object)val == (Object)null || ((Actor)val).isDead || (Object)(object)((Actor)val).ai == (Object)null) { flag = true; } else if (Time.time > value.Until) { flag = true; } else if (((Actor)val).ai.restrained) { flag = true; } else if (Vector3.Distance(((Component)val).transform.position, ((Component)instance).transform.position) > ModConfig.FollowGiveUpDistance.Value) { flag = true; } } catch { flag = true; } if (flag) { (list ?? (list = new List<int>())).Add(item.Key); } else { if (Time.time < value.NextNudge) { continue; } value.NextNudge = Time.time + ModConfig.FollowNudgeInterval.Value; try { NewNode currentNode = ((Actor)instance).currentNode; if (currentNode != null) { ((Actor)val).ai.SetInvestigationUrgency((InvestigationUrgency)0); ((Actor)val).ai.Investigate(currentNode, ((Component)instance).transform.position, (Actor)null, (ReactionState)0, 1f, 0, false, 1f, (Interactable)null); } } catch (Exception ex) { if (ModConfig.VerboseLogging.Value) { Plugin.Log.LogWarning((object)("Could not nudge a follower: " + ex.Message)); } (list ?? (list = new List<int>())).Add(item.Key); } } } if (list == null) { return; } foreach (int item2 in list) { try { Citizen val2 = Resolve(item2); if ((Object)(object)val2 != (Object)null) { SessionLog.Note(((Human)val2).GetCitizenName() + " stopped following you."); } } catch { } Following.Remove(item2); } } } public static class GoalDirector { private static readonly Dictionary<string, string[]> Intents = new Dictionary<string, string[]> { { "go_home", new string[4] { "gohome", "home", "returnhome", "gotohome" } }, { "go_to_work", new string[3] { "work", "gotowork", "job" } }, { "go_to_bed", new string[3] { "bed", "sleep", "gotobed" } }, { "leave", new string[5] { "leave", "exit", "gooutside", "wander", "walk" } } }; public static IEnumerable<string> IntentNames() { return Intents.Keys; } public static string Send(Citizen citizen, string intent) { if (!ModConfig.AllowGoalRedirection.Value) { return "changing what people are doing is switched off"; } if ((Object)(object)citizen == (Object)null || (Object)(object)((Actor)citizen).ai == (Object)null) { return "no AI on this citizen"; } if (string.IsNullOrWhiteSpace(intent)) { return "no destination given"; } if (!Intents.TryGetValue(intent.Trim().ToLowerInvariant(), out var value)) { return "not a destination this mod knows"; } AIGoalPreset val = FindPreset(value); if ((SoCustomComparison)(object)val == (SoCustomComparison)null) { return "the game has no goal preset matching " + intent + " - run the goal dump in Debug"; } try { float num = (((Object)(object)SessionData.Instance != (Object)null) ? SessionData.Instance.gameTime : 0f); NewAIGoal val2 = ((Actor)citizen).ai.CreateNewGoal(val, num, 0f, (NewNode)null, (Interactable)null, (NewGameLocation)null, (SocialGroup)null, (Murder)null, -2); if (val2 == null) { return "the game refused to create the goal"; } try { val2.UpdatePriority(false); } catch { } return null; } catch (Exception ex) { return "creating the goal threw: " + ex.Message; } } public static string InvestigateHere(Citizen citizen, bool urgent) { //IL_009d: Unknown result type (might be due to invalid IL or missing references) if (!ModConfig.AllowGoalRedirection.Value) { return "changing what people are doing is switched off"; } if ((Object)(object)citizen == (Object)null || (Object)(object)((Actor)citizen).ai == (Object)null) { return "no AI on this citizen"; } Player instance = Player.Instance; if ((Object)(object)instance == (Object)null) { return "no player to investigate"; } try { NewNode currentNode = ((Actor)instance).currentNode; if (currentNode == null) { return "there is no node to send them to"; } ((Actor)citizen).ai.SetInvestigationUrgency((InvestigationUrgency)(urgent ? 1 : 0)); ((Actor)citizen).ai.Investigate(currentNode, ((Component)instance).transform.position, (Actor)null, (ReactionState)2, 1f, 0, urgent, 1f, (Interactable)null); return null; } catch (Exception ex) { return "sending them to look threw: " + ex.Message; } } private static AIGoalPreset FindPreset(string[] keywords) { try { List<AIGoalPreset> val = (((Object)(object)Toolbox.Instance != (Object)null) ? Toolbox.Instance.allGoals : null); if (val == null) { return null; } foreach (string value in keywords) { Enumerator<AIGoalPreset> enumerator = val.GetEnumerator(); while (enumerator.MoveNext()) { AIGoalPreset current = enumerator.Current; if (!((SoCustomComparison)(object)current == (SoCustomComparison)null)) { string text = PresetName(current); if (!string.IsNullOrEmpty(text) && text.Replace(" ", "").Replace("_", "").ToLowerInvariant() .Contains(value)) { return current; } } } } } catch (Exception ex) { Plugin.Log.LogWarning((object)("Goal preset lookup failed: " + ex.Message)); } return null; } private static string PresetName(AIGoalPreset preset) { try { if (!string.IsNullOrEmpty(((SoCustomComparison)preset).presetName)) { return ((SoCustomComparison)preset).presetName; } } catch { } try { return ((Object)preset).name; } catch { return null; } } public static string DumpPresetNames() { try { List<AIGoalPreset> val = (((Object)(object)Toolbox.Instance != (Object)null) ? Toolbox.Instance.allGoals : null); if (val == null) { return "Toolbox has no goal list yet - load a save first."; } List<string> list = new List<string>(); Enumerator<AIGoalPreset> enumerator = val.GetEnumerator(); while (enumerator.MoveNext()) { AIGoalPreset current = enumerator.Current; string text = PresetName(current); if (!string.IsNullOrEmpty(text)) { list.Add(text); } } list.Sort(); StringBuilder stringBuilder = new StringBuilder(); stringBuilder.AppendLine("Goal presets the game has (" + list.Count + "):"); foreach (string item in list) { stringBuilder.AppendLine(" " + item); } SessionLog.Note(stringBuilder.ToString()); List<string> list2 = new List<string>(); foreach (KeyValuePair<string, string[]> intent in Intents) { AIGoalPreset val2 = FindPreset(intent.Value); list2.Add(intent.Key + " -> " + (((SoCustomComparison)(object)val2 != (SoCustomComparison)null) ? PresetName(val2) : "NOTHING MATCHED")); } SessionLog.Note("Intent mapping: " + string.Join(", ", list2)); return list.Count + " presets written to the transcript."; } catch (Exception ex) { return "Dump failed: " + ex.Message; } } } public static class Invitation { private const int FillerWordLimit = 6; private static readonly string[] Asking = new string[40] { "who", "what", "where", "when", "why", "which", "how", "did", "do ", "does", "have", "has", "is ", "are ", "was ", "were ", "can ", "could", "tell", "say", "ask", "saw", "seen", "see", "spot", "notice", "remember", "describe", "name", "anyone", "anybody", "someone", "somebody", "quem", "onde", "quando", "porque", "viu", "diga", "diz" }; public static bool WasInvited(string playerLine) { if (string.IsNullOrWhiteSpace(playerLine)) { return false; } string text = playerLine.Trim(); if (text.IndexOf('?') >= 0) { return true; } string[] array = text.Split(new char[4] { ' ', '\t', '\n', '\r' }, StringSplitOptions.RemoveEmptyEntries); if (array.Length > 6) { return true; } string text2 = " " + text.ToLowerInvariant() + " "; string[] asking = Asking; foreach (string value in asking) { if (text2.Contains(value)) { return true; } } return false; } } public static class Negotiation { private sealed class Demand { public int Amount; public string For; public float MadeAt; } private static readonly Dictionary<int, Demand> Outstanding = new Dictionary<int, Demand>(); public static string PendingFor(Citizen citizen) { if ((Object)(object)citizen == (Object)null) { return null; } if (!Outstanding.TryGetValue(((Human)citizen).humanID, out var value)) { return null; } if (Time.time - value.MadeAt > ModConfig.DemandExpiry.Value) { Outstanding.Remove(((Human)citizen).humanID); return null; } return "You have asked this investigator for $" + value.Amount + (string.IsNullOrWhiteSpace(value.For) ? "" : (" in return for " + value.For)) + ", and they have not paid yet."; } public static string Demand_(Citizen citizen, string amountText, string forWhat) { if (!ModConfig.AllowNegotiation.Value) { return "haggling is switched off"; } if ((Object)(object)citizen == (Object)null) { return "nobody to ask"; } int num = ParseAmount(amountText); if (num <= 0) { return "no price named"; } int value = ModConfig.MaxDemand.Value; if (num > value) { num = value; } Outstanding[((Human)citizen).humanID] = new Demand { Amount = num, For = forWhat, MadeAt = Time.time }; WorldMemory.Save(); return null; } public static string TakePayment(Citizen citizen) { //IL_0102: Unknown result type (might be due to invalid IL or missing references) //IL_0108: Invalid comparison between Unknown and I4 if (!ModConfig.AllowNegotiation.Value) { return "haggling is switched off"; } if ((Object)(object)citizen == (Object)null) { return "nobody to pay"; } if (!Outstanding.TryGetValue(((Human)citizen).humanID, out var value)) { return "they never named a price, so there is nothing to settle"; } try { GameplayController instance = GameplayController.Instance; if ((Object)(object)instance == (Object)null) { return "no gameplay controller to take it from"; } if (instance.money < value.Amount) { return "the investigator does not have $" + value.Amount; } instance.AddMoney(-value.Amount, true, "paid to " + ((Human)citizen).GetCitizenName()); try { List<WalletItem> walletItems = ((Human)citizen).walletItems; if (walletItems != null) { bool flag = false; Enumerator<WalletItem> enumerator = walletItems.GetEnumerator(); while (enumerator.MoveNext()) { WalletItem current = enumerator.Current; if (current == null || (int)current.itemType != 1) { continue; } current.money += value.Amount; flag = true; break; } if (!flag && ModConfig.VerboseLogging.Value) { Plugin.Log.LogInfo((object)("Paid " + ((Human)citizen).GetCitizenName() + " but they had no cash entry to add it to.")); } } } catch { } Outstanding.Remove(((Human)citizen).humanID); WorldMemory.Save(); try { Player instance2 = Player.Instance; Acquaintance val = default(Acquaintance); if ((Object)(object)instance2 != (Object)null && ((Human)citizen).FindAcquaintanceExists((Human)(object)instance2, ref val) && val != null) { val.like = Mathf.Clamp01(val.like + ModConfig.PaymentGoodwill.Value); } } catch { } SessionLog.Note("Paid " + ((Human)citizen).GetCitizenName() + " $" + value.Amount + (string.IsNullOrWhiteSpace(value.For) ? "" : (" for " + value.For)) + "."); return null; } catch (Exception ex) { return "the payment threw: " + ex.Message; } } public static void Clear() { Outstanding.Clear(); } public static Dictionary<string, WorldMemory.Owed> Export() { Dictionary<string, WorldMemory.Owed> dictionary = new Dictionary<string, WorldMemory.Owed>(); foreach (KeyValuePair<int, Demand> item in Outstanding) { dictionary[item.Key.ToString()] = new WorldMemory.Owed { Amount = item.Value.Amount, For = item.Value.For }; } return dictionary; } public static void Restore(Dictionary<string, WorldMemory.Owed> saved) { Outstanding.Clear(); if (saved == null) { return; } foreach (KeyValuePair<string, WorldMemory.Owed> item in saved) { if (int.TryParse(item.Key, out var result) && item.Value != null) { Outstanding[result] = new Demand { Amount = item.Value.Amount, For = item.Value.For, MadeAt = Time.time }; } } } private static int ParseAmount(string text) { if (string.IsNullOrWhiteSpace(text)) { return 0; } string text2 = ""; for (int i = 0; i < text.Length; i++) { char c = text[i]; if (c >= '0' && c <= '9') { text2 += c; } else if (text2.Length > 0) { break; } } int result; return int.TryParse(text2, out result) ? result : 0; } } public static class Opinion { public static string Shift(Citizen speaker, string targetName, float delta, bool shouted) { if (!ModConfig.AllowThirdPartyOpinion.Value) { return "changing how people see each other is switched off"; } if ((Object)(object)speaker == (Object)null) { return "nobody to persuade"; } if (string.IsNullOrWhiteSpace(targetName)) { return "no name given"; } Human target; string text = Resolve(speaker, targetName, shouted, out target); if (text != null) { return text; } try { Acquaintance val = default(Acquaintance); if (!((Human)speaker).FindAcquaintanceExists(target, ref val) || val == null) { return "they do not know that person well enough to have a view"; } float value = ModConfig.MaxOpinionShiftPerLine.Value; float num = Mathf.Lerp(1f, 1f - ModConfig.LoyaltyResistance.Value, Mathf.Clamp01(val.known)); float num2 = Mathf.Clamp(delta, 0f - value, value) * num; if (Mathf.Abs(num2) < 0.005f) { return "they are too close to that person to be swayed by a sentence"; } float like = val.like; val.like = Mathf.Clamp01(val.like + num2); if (Mathf.Abs(val.like - like) < 0.001f) { return "their view of that person is already at its limit"; } SessionLog.Note(((Human)speaker).GetCitizenName() + "'s view of " + target.GetCitizenName() + " moved " + ((num2 > 0f) ? "+" : "") + num2.ToString("0.00") + "."); return null; } catch (Exception ex) { return "changing their view threw: " + ex.Message; } } public static string StandUpFor(Citizen speaker, string targetName, bool shouted) { if (!ModConfig.AllowThirdPartyOpinion.Value) { return "taking somebody else's side is switched off"; } if ((Object)(object)speaker == (Object)null || (Object)(object)((Actor)speaker).ai == (Object)null) { return "no AI on this citizen"; } if (string.IsNullOrWhiteSpace(targetName)) { return "no name given"; } Human target; string text = Resolve(speaker, targetName, shouted, out target); if (text != null) { return text; } Citizen val = TryCitizen(target); try { if ((Object)(object)val != (Object)null && !((Actor)speaker).ai.restrained && !((Actor)speaker).ai.inCombat) { foreach (Citizen item in Earshot.CitizensWhoCanHear((Actor)(object)speaker, shouted)) { if (!((Object)(object)item == (Object)null) && !((Object)(object)((Actor)item).ai == (Object)null) && ((Actor)item).ai.inCombat && ((Human)item).humanID != ((Human)speaker).humanID) { Actor attackTarget = ((Actor)item).ai.attackTarget; if (!((Object)(object)attackTarget == (Object)null) && !(((Il2CppObjectBase)attackTarget).Pointer != ((Il2CppObjectBase)target).Pointer)) { ((Actor)speaker).ai.SetInCombat(true, false); ((Actor)speaker).ai.StartAttack((Actor)(object)item); SessionLog.Note(((Human)speaker).GetCitizenName() + " stepped in for " + target.GetCitizenName() + "."); return null; } } } } } catch { } if ((Object)(object)val != (Object)null) { int num = 0; foreach (Citizen item2 in Earshot.CitizensWhoCanHear((Actor)(object)speaker, shouted)) { try { if (!((Object)(object)item2 == (Object)null) && !((Object)(object)((Actor)item2).ai == (Object)null) && ((Actor)item2).isEnforcer && ((Actor)item2).ai.persuit) { Actor persuitTarget = ((Actor)item2).ai.persuitTarget; if (!((Object)(object)persuitTarget == (Object)null) && !(((Il2CppObjectBase)persuitTarget).Pointer != ((Il2CppObjectBase)target).Pointer)) { ((Actor)item2).ai.CancelPersue(); num++; } } } catch { } } if (num > 0) { SessionLog.Note(((Human)speaker).GetCitizenName() + " called the police off " + target.GetCitizenName() + "."); return null; } } return "nobody is threatening that person right now"; } public static List<string> KnownPeople(Citizen speaker, int max = 6) { List<string> list = new List<string>(); if ((Object)(object)speaker == (Object)null) { return list; } try { if (((Human)speaker).acquaintances == null) { return list; } Enumerator<Acquaintance> enumerator = ((Human)speaker).acquaintances.GetEnumerator(); while (enumerator.MoveNext()) { Acquaintance current = enumerator.Current; if (current == null || current.known < 0.3f) { continue; } Human other = current.GetOther((Human)(object)speaker); if ((Object)(object)other == (Object)null || ((Actor)other).isPlayer) { continue; } string citizenName = other.GetCitizenName(); if (!string.IsNullOrEmpty(citizenName)) { list.Add(citizenName + " (" + Feeling(current.like) + ")"); if (list.Count >= max) { break; } } } } catch { } return list; } private static string Feeling(float like) { if (like < 0.2f) { return "cannot stand them"; } if (like < 0.4f) { return "wary of them"; } if (like < 0.6f) { return "neutral"; } if (like < 0.8f) { return "fond of them"; } return "very close"; } private static string Resolve(Citizen speaker, string targetName, bool shouted, out Human target) { target = null; string wanted = targetName.Trim(); try { foreach (Citizen item in Earshot.CitizensWhoCanHear((Actor)(object)speaker, shouted)) { if ((Object)(object)item == (Object)null || ((Actor)item).isPlayer || ((Human)item).humanID == ((Human)speaker).humanID || !Matches(((Human)item).GetCitizenName(), wanted)) { continue; } target = (Human)(object)item; return null; } if (((Human)speaker).acquaintances != null) { Enumerator<Acquaintance> enumerator2 = ((Human)speaker).acquaintances.GetEnumerator(); while (enumerator2.MoveNext()) { Acquaintance current2 = enumerator2.Current; if (current2 != null) { Human other = current2.GetOther((Human)(object)speaker); if (!((Object)(object)other == (Object)null) && !((Actor)other).isPlayer && Matches(other.GetCitizenName(), wanted)) { target = other; return null; } } } } } catch (Exception ex) { return "looking that person up threw: " + ex.Message; } return "they do not know anybody by that name"; } private static bool Matches(string name, string wanted) { return !string.IsNullOrEmpty(name) && name.IndexOf(wanted, StringComparison.OrdinalIgnoreCase) >= 0; } private static Citizen TryCitizen(Human human) { try { return (human != null) ? ((Il2CppObjectBase)human).TryCast<Citizen>() : null; } catch { return null; } } } public static class Testimony { public static string RevealSighting(Citizen witness, string targetName) { if (!ModConfig.AllowTestimony.Value) { return "giving up sightings is switched off"; } if ((Object)(object)witness == (Object)null) { return "no witness"; } if (string.IsNullOrWhiteSpace(targetName)) { return "no name given"; } Human subject; string text = FindSubject(witness, targetName, out subject); if (text != null) { return text; } try { ((Human)witness).RevealSighting(subject, false, true, ((Actor)witness).speechController, true); return null; } catch (Exception ex) { return "the game refused the testimony: " + ex.Message; } } public static List<string> PossibleSubjects(Citizen witness, int max = 6) { List<string> list = new List<string>(); if ((Object)(object)witness == (Object)null) { return list; } try { if (((Human)witness).lastSightings == null) { return list; } Enumerator<Human, Sighting> enumerator = ((Human)witness).lastSightings.GetEnumerator(); while (enumerator.MoveNext()) { KeyValuePair<Human, Sighting> current = enumerator.Current; Human key = current.Key; if ((Object)(object)key == (Object)null) { continue; } string citizenName = key.GetCitizenName(); if (!string.IsNullOrEmpty(citizenName)) { list.Add(citizenName); if (list.Count >= max) { break; } } } } catch (Exception ex) { if (ModConfig.VerboseLogging.Value) { Plugin.Log.LogWarning((object)("Could not list who they could testify about: " + ex.Message)); } } return list; } private static string FindSubject(Citizen witness, string targetName, out Human subject) { subject = null; string value = targetName.Trim(); try { if (((Human)witness).lastSightings == null || ((Human)witness).lastSightings.Count == 0) { return "they have not seen anybody worth mentioning"; } Enumerator<Human, Sighting> enumerator = ((Human)witness).lastSightings.GetEnumerator(); while (enumerator.MoveNext()) { KeyValuePair<Human, Sighting> current = enumerator.Current; Human key = current.Key; if (!((Object)(object)key == (Object)null)) { string citizenName = key.GetCitizenName(); if (!string.IsNullOrEmpty(citizenName) && citizenName.IndexOf(value, StringComparison.OrdinalIgnoreCase) >= 0) { subject = key; return null; } } } } catch (Exception ex) { return "checking who they saw threw: " + ex.Message; } return "they never saw that person"; } } public enum VoiceLevel { Whisper, Normal, Shout } public static class Voice { public static float RadiusOf(VoiceLevel level) { return level switch { VoiceLevel.Whisper => ModConfig.WhisperRadius.Value, VoiceLevel.Shout => ModConfig.ShoutRadius.Value, _ => ModConfig.TalkRadius.Value, }; } public static bool CarriesNextDoor(VoiceLevel level) { return level == VoiceLevel.Shout; } public static string Describe(VoiceLevel level) { return level switch { VoiceLevel.Whisper => "whispered", VoiceLevel.Shout => "shouted", _ => "spoken", }; } public static VoiceLevel Parse(string written, VoiceLevel fallback = VoiceLevel.Normal) { if (string.IsNullOrWhiteSpace(written)) { return fallback; } switch (written.Trim().ToLowerInvariant()) { case "whisper": case "whispered": case "whispering": case "quiet": case "quietly": case "under_my_breath": case "murmur": return VoiceLevel.Whisper; case "shout": case "shouted": case "shouting": case "yell": case "yelled": case "scream": case "screamed": case "loud": case "loudly": case "call_out": return VoiceLevel.Shout; case "normal": case "speak": case "spoken": case "say": case "said": case "talk": return VoiceLevel.Normal; default: return fallback; } } public static VoiceLevel FromShouted(bool shouted) { return (!shouted) ? VoiceLevel.Normal : VoiceLevel.Shout; } public static bool IsShout(VoiceLevel level) { return level == VoiceLevel.Shout; } } public static class WalletReader { public static List<string> Describe(Citizen citizen) { //IL_0060: Unknown result type (might be due to invalid IL or missing references) //IL_0065: Unknown result type (might be due to invalid IL or missing references) //IL_0067: Unknown result type (might be due to invalid IL or missing references) //IL_0069: Unknown result type (might be due to invalid IL or missing references) //IL_006b: Unknown result type (might be due to invalid IL or missing references) //IL_006e: Unknown result type (might be due to invalid IL or missing references) //IL_0080: Expected I4, but got Unknown List<string> list = new List<string>(); if ((Object)(object)citizen == (Object)null) { return list; } try { List<WalletItem> walletItems = ((Human)citizen).walletItems; if (walletItems == null) { return list; } int num = 0; int num2 = 0; int num3 = 0; Enumerator<WalletItem> enumerator = walletItems.GetEnumerator(); while (enumerator.MoveNext()) { WalletItem current = enumerator.Current; if (current != null) { WalletItemType itemType = current.itemType; WalletItemType val = itemType; switch (val - 1) { case 0: num += current.money; break; case 2: num2++; break; case 1: num3++; break; } } } if (num > 0) { list.Add("You are carrying $" + num + " in cash."); } if (num2 > 0) { list.Add("You are carrying " + num2 + ((num2 == 1) ? " key." : " keys.")); } if (num3 > 0) { list.Add("You are carrying " + num3 + ((num3 == 1) ? " piece of paperwork." : " pieces of paperwork.")); } if (num <= 0 && num2 == 0 && num3 == 0) { list.Add("Your pockets are empty."); } } catch (Exception ex) { if (ModConfig.VerboseLogging.Value) { Plugin.Log.LogWarning((object)("Could not read a wallet: " + ex.Message)); } } return list; } public static int CashOn(Citizen citizen) { //IL_0038: Unknown result type (might be due to invalid IL or missing references) //IL_003e: Invalid comparison between Unknown and I4 int num = 0; try { List<WalletItem> val = ((citizen != null) ? ((Human)citizen).walletItems : null); if (val == null) { return 0; } Enumerator<WalletItem> enumerator = val.GetEnumerator(); while (enumerator.MoveNext()) { WalletItem current = enumerator.Current; if (current != null && (int)current.itemType == 1) { num += current.money; } } } catch { } return num; } public static string GiveMoney(Citizen citizen, string requested) { if (!ModConfig.AllowMoneyHandover.Value) { return "handing over money is switched off"; } if ((Object)(object)citizen == (Object)null) { return "nobody to take it from"; } int num = CashOn(citizen); if (num <= 0) { return "they have no cash on them"; } int num2 = ParseAmount(requested); if (num2 <= 0) { num2 = num; } int value = ModConfig.MaxMoneyPerLine.Value; int num3 = Math.Min(Math.Min(num2, num), value); if (num3 <= 0) { return "nothing left to give"; } try { if (!TakeFromWallet(citizen, num3)) { return "their cash could not be taken"; } GameplayController instance = GameplayController.Instance; if ((Object)(object)instance == (Object)null) { return "no gameplay controller to receive it"; } instance.AddMoney(num3, true, ((Human)citizen).GetCitizenName() + " handed it over"); return null; } catch (Exception ex) { return "the handover threw: " + ex.Message; } } private static bool TakeFromWallet(Citizen citizen, int amount) { //IL_0043: Unknown result type (might be due to invalid IL or missing references) //IL_0049: Invalid comparison between Unknown and I4 int num = amount; List<WalletItem> walletItems = ((Human)citizen).walletItems; if (walletItems == null) { return false; } Enumerator<WalletItem> enumerator = walletItems.GetEnumerator(); while (enumerator.MoveNext()) { WalletItem current = enumerator.Current; if (num <= 0) { break; } if (current != null && (int)current.itemType == 1 && current.money > 0) { int num2 = Math.Min(current.money, num); current.money -= num2; num -= num2; } } return num < amount; } private static int ParseAmount(string text) { if (string.IsNullOrWhiteSpace(text)) { return 0; } string text2 = ""; for (int i = 0; i < text.Length; i++) { char c = text[i]; if (c >= '0' && c <= '9') { text2 += c; } else if (text2.Length > 0) { break; } } int result; return int.TryParse(text2, out result) ? result : 0; } } public static class WorldEffectExecutor { public sealed class EffectReport { public readonly List<string> Applied = new List<string>(); public readonly List<string> Rejected = new List<string>(); public void Reject(string effect, string reason) { Rejected.Add(effect + " (" + reason + ")"); } } private static bool _registered; public static void RegisterAll() { if (!_registered) { _registered = true; Add("end_conversation", "you walk away and refuse to keep talking", (EffectCatalogue.Request r) => EndConversation(r.Speaker)); Add("calm_down", "you settle, becoming less alarmed", (EffectCatalogue.Request r) => ShiftAlertness(r.Speaker, -0.3f), null, "mood", new string[2] { "calm", "relax" }); Add("alarm", "you become noticeably more frightened", (EffectCatalogue.Request r) => ShiftAlertness(r.Speaker, 0.3f), null, "mood", new string[2] { "panic", "get_scared" }); Add("answer_door", "you go and open the door", (EffectCatalogue.Request r) => AnswerDoor(r.Speaker)); Add("flee", "you turn and run; put a name in target to get away from somebody else", (EffectCatalogue.Request r) => Flee(r.Speaker, r.Target), () => ModConfig.AllowCombatEffects.Value, "stance", new string[3] { "run", "run_away", "escape" }); Add("attack_the_investigator", "you attack the investigator you are talking to", (EffectCatalogue.Request r) => Attack(r.Speaker, null, r.Shouted), () => ModConfig.AllowCombatEffects.Value, "stance", new string[2] { "attack_them", "attack_the_player" }, Disposition.WouldFight); Add("attack_someone_else", "you attack somebody else here; put their name in target", (EffectCatalogue.Request r) => string.IsNullOrWhiteSpace(r.Target) ? "no name given - use attack_the_investigator if you mean them" : Attack(r.Speaker, r.Target, r.Shouted), () => ModConfig.AllowCombatEffects.Value, "stance", new string[3] { "fight", "assault", "strike" }); Add("attack", null, (EffectCatalogue.Request r) => string.IsNullOrWhiteSpace(r.Target) ? "ambiguous - use attack_the_investigator or attack_someone_else" : Attack(r.Speaker, r.Target, r.Shouted), () => ModConfig.AllowCombatEffects.Value, "stance"); Add("surrender", "you stop fighting and give yourself up", (EffectCatalogue.Request r) => Surrender(r.Speaker), () => ModConfig.AllowCombatEffects.Value, "stance", new string[2] { "give_up", "yield" }); Add("give_item", "you hand over the item you are holding", (EffectCatalogue.Request r) => GiveHeldItem(r.Speaker), () => ModConfig.AllowItemHandover.Value, null, new string[3] { "hand_over", "give", "give_object" }, (CitizenSnapshot s) => s?.CitizenIsArmed ?? true); Add("give_money", "you hand over cash you are carrying; put the amount in target", (EffectCatalogue.Request r) => WalletReader.GiveMoney(r.Speaker, r.Target), () => ModConfig.AllowMoneyHandover.Value, null, new string[3] { "give_cash", "pay_them", "hand_over_money" }, (CitizenSnapshot s) => s?.HasCash ?? true); Add("side_with_them", "you decide you are on this investigator's side and will back them up", (EffectCatalogue.Request r) => Allegiance.SideWith(r.Speaker), () => ModConfig.AllowAllegiance.Value, "allegiance", new string[3] { "ally", "join_them", "help_them" }); Add("turn_against_them", "you decide you are against this investigator", (EffectCatalogue.Request r) => Allegiance.TurnAgainst(r.Speaker), () => ModConfig.AllowAllegiance.Value, "allegiance", new string[2] { "oppose_them", "become_hostile" }); Add("warn_them_against", "your own opinion of somebody drops because of what you just heard about them; name them in target", (EffectCatalogue.Request r) => Opinion.Shift(r.Speaker, r.Target, 0f - ModConfig.MaxOpinionShiftPerLine.Value, r.Shouted), () => ModConfig.AllowThirdPartyOpinion.Value, "opinion", new string[3] { "badmouth", "poison_against", "turn_against_someone" }, Disposition.WouldTalkAboutOthers); Add("speak_well_of", "you raise their opinion of somebody else; put that person's name in target", (EffectCatalogue.Request r) => Opinion.Shift(r.Speaker, r.Target, ModConfig.MaxOpinionShiftPerLine.Value, r.Shouted), () => ModConfig.AllowThirdPartyOpinion.Value, "opinion", new string[2] { "vouch_for_someone", "praise" }, Disposition.WouldTalkAboutOthers); Add("stand_up_for", "you take somebody else's side against whoever is threatening them; name them in target", (EffectCatalogue.Request r) => Opinion.StandUpFor(r.Speaker, r.Target, r.Shouted), () => ModConfig.AllowThirdPartyOpinion.Value, null, new string[2] { "defend_someone", "protect_someone" }); Add("name_a_price", "you will talk, for money; put the amount in target and what for in detail", (EffectCatalogue.Request r) => Negotiation.Demand_(r.Speaker, r.Target, r.Detail), () => ModConfig.AllowNegotiation.Value, "deal", new string[3] { "demand_payment", "ask_for_money", "set_price" }, Disposition.WouldHaggle); Add("take_the_money", "they have agreed to a price you already named, so you take it", (EffectCatalogue.Request r) => Negotiation.TakePayment(r.Speaker), () => ModConfig.AllowNegotiation.Value, "deal", new string[2] { "accept_payment", "take_payment" }); Add("follow", "you agree to come along with the investigator", (EffectCatalogue.Request r) => FollowDirector.Start(r.Speaker), () => ModConfig.AllowFollowing.Value, "escort", new string[3] { "follow_them", "come_along", "accompany" }); Add("stop_following", "you have had enough and stop going with them", (EffectCatalogue.Request r) => FollowDirector.Stop(r.Speaker), () => ModConfig.AllowFollowing.Value, "escort", new string[2] { "leave_them", "stop_follow" }); Add("report_the_investigator", "you turn the police on the investigator themselves", (EffectCatalogue.Request r) => SetOfficerPursuit(r.Speaker, (Actor)(object)Player.Instance, r.Shouted), () => ModConfig.AllowPoliceRedirection.Value, "police", new string[2] { "report_them", "report_the_player" }); Add("send_police_after", "you have somebody standing here right now chased down by the police; name them in target", (EffectCatalogue.Request r) => AccuseOther(r.Speaker, r.Target, r.Shouted), () => ModConfig.AllowPoliceRedirection.Value, "police", new string[1] { "accuse" }); Add("call_police_off", "you call the police off the investigator", (EffectCatalogue.Request r) => CallOffOfficers(r.Speaker, r.Shouted), () => ModConfig.AllowPoliceRedirection.Value, "police", new string[2] { "protect", "vouch_for_them" }); Add("call_police", null, (EffectCatalogue.Request r) => string.IsNullOrWhiteSpace(r.Target) ? "ambiguous - use report_the_investigator or send_police_after" : AccuseOther(r.Speaker, r.Target, r.Shouted), () => ModConfig.AllowPoliceRedirection.Value, "police"); Add("tell_what_i_saw", "you tell them about somebody you saw earlier, which puts a real lead in their case file; name that person in target", (EffectCatalogue.Request r) => Testimony.RevealSighting(r.Speaker, r.Target), () => ModConfig.AllowTestimony.Value, null, new string[3] { "testify", "reveal_sighting", "tell_what_i_know" }, (CitizenSnapshot s) => s == null || s.CanTestifyAbout.Count > 0, needsInvitation: true); Add("give_up_a_detail", "you tell them something about yourself and it goes into their case file; put name, address, job, workplace, partner or phone in target", (EffectCatalogue.Request r) => Disclosure.Reveal(r.Speaker, r.Target), () => ModConfig.AllowDisclosure.Value, null, new string[3] { "tell_them_about_myself", "disclose", "file_a_detail" }, (CitizenSnapshot s) => s == null || s.CanDisclose.Count > 0, needsInvitation: true); Add("go", "you drop what you were doing and leave; put go_home, go_to_work, go_to_bed or leave in target", (EffectCatalogue.Request r) => GoalDirector.Send(r.Speaker, r.Target), () => ModConfig.AllowGoalRedirection.Value, "errand", new string[3] { "leave", "go_away", "depart" }); Add("come_and_look", "you go over to see what the fuss is about", (EffectCatalogue.Request r) => GoalDirector.InvestigateHere(r.Speaker, r.Shouted), () => ModConfig.AllowGoalRedirection.Value, "errand", new string[2] { "investigate", "come_over" }); Add("crowd_panic", "everyone who heard you scatters", (EffectCatalogue.Request r) => CrowdEffects.Panic(r.Speaker, r.Shouted), () => ModConfig.AllowCrowdEffects.Value, "crowd", null, (CitizenSnapshot s) => s == null || s.Bystanders.Count > 0); Add("crowd_settle", "everyone who heard you calms down", (EffectCatalogue.Request r) => CrowdEffects.Settle(r.Speaker, r.Shouted), () => ModConfig.AllowCrowdEffects.Value, "crowd", null, (CitizenSnapshot s) => s == null || s.Bystanders.Count > 0); Add("crowd_gather", "everyone who heard you comes over to look", (EffectCatalogue.Request r) => CrowdEffects.Gather(r.Speaker, r.Shouted), () => ModConfig.AllowCrowdEffects.Value, "crowd", null, (CitizenSnapshot s) => s == null || s.Bystanders.Count > 0); } } private static void Add(string name, string description, Func<EffectCatalogue.Request, string> run, Func<bool> gate = null, string conflicts = null, string[] aliases = null, Func<CitizenSnapshot, bool> relevant = null, bool needsInvitation = false) { EffectCatalogue.Definition definition = new EffectCatalogue.Definition(); definition.Name = name; definition.Description = description; definition.Run = run; definition.Enabled = gate ?? ((Func<bool>)(() => true)); definition.Conflicts = conflicts; definition.Aliases = aliases ?? new string[0]; definition.Relevant = relevant; definition.NeedsInvitation = needsInvitation; EffectCatalogue.Register(definition); } public static IEnumerable<string> PermittedEffectNames(CitizenSnapshot snapshot = null) { if (!ModConfig.EnableWorldEffects.Value) { yield break; } RegisterAll(); foreach (EffectCatalogue.Definition definition in EffectCatalogue.Offered(snapshot)) { yield return definition.Name + " - " + definition.Description; } } public static EffectReport Apply(Citizen speaker, NpcReply reply, bool shouted, string playerLine = null) { EffectReport effectReport = new EffectReport(); if (reply == null) { return effectReport; } if (!IsUsable(speaker)) { effectReport.Reject("everything", "the person is gone"); return effectReport; } RegisterAll(); ApplyRelationship(speaker, reply, effectReport); if (!ModConfig.EnableWorldEffects.Value) { if (reply.Effects != null && reply.Effects.Count > 0) { effectReport.Reject("all effects", "world effects are switched off"); } return effectReport; } ApplyAlarm(speaker, reply, effectReport); if (reply.Effects == null) { return effectReport; } HashSet<string> hashSet = new HashSet<string>(); Dictionary<string, string> dictionary = new Dictionary<string, string>(); foreach (WorldEffect effect in reply.Effects) { if (effect == null || string.IsNullOrWhiteSpace(effect.Type)) { continue; } string text = effect.Type.Trim(); EffectCatalogue.Definition definition = EffectCatalogue.Find(text); if (definition == null) { effectReport.Reject(text, "not an effect this mod knows"); } else if (!definition.Enabled()) { effectReport.Reject(definition.Name, "switched off in the settings"); } else { if (!hashSet.Add(definition.Name)) { continue; } if (!string.IsNullOrEmpty(definition.Conflicts)) { if (dictionary.TryGetValue(definition.Conflicts, out var value)) { effectReport.Reject(definition.Name, "contradicts " + value); continue; } dictionary[definition.Conflicts] = definition.Name; } if (definition.NeedsInvitation && !Invitation.WasInvited(playerLine)) { effectReport.Reject(definition.Name, "nobody asked"); continue; } try { string text2 = definition.Run(new EffectCatalogue.Request { Speaker = speaker, Target = effect.Target, Detail = effect.Detail, Shouted = shouted, PlayerLine = playerLine }); if (text2 == null) { effectReport.Applied.Add(definition.Name); } else { effectReport.Reject(definition.Name, text2); } } catch (Exception ex) { effectReport.Reject(definition.Name, "threw: " + ex.Message); Plugin.Log.LogWarning((object)("Effect " + definition.Name + " threw: " + ex.Message)); } } } return effectReport; } private static bool IsUsable(Citizen citizen) { if ((Object)(object)citizen == (Object)null) { return false; } try { if (((Il2CppObjectBase)citizen).Pointer == IntPtr.Zero) { return false; } int humanID = ((Human)citizen).humanID; return !((Actor)citizen).isDead; } catch { return false; } } private static void ApplyRelationship(Citizen speaker, NpcReply reply, EffectReport report) { RelationshipDelta relationshipDelta = reply.RelationshipDelta; if (relationshipDelta == null) { return; } try { Player instance = Player.Instance; if ((Object)(object)instance == (Object)null) { return; } Acquaintance val = default(Acquaintance); if (!((Human)speaker).FindAcquaintanceExists((Human)(object)instance, ref val) || val == null) { if (Mathf.Abs(relationshipDelta.Known) > 0.001f || Mathf.Abs(relationshipDelta.Like) > 0.001f) { ((Human)speaker).AddAcquaintance((Human)(object)instance, Mathf.Clamp(relationshipDelta.Known, 0f, 0.2f), (ConnectionType)12, true, false, (ConnectionType)0, (SocialGroup)null); report.Applied.Add("met the investigator"); } return; } float value = ModConfig.MaxLikeShiftPerLine.Value; float num = Mathf.Clamp(relationshipDelta.Like, 0f - value, value); if (Mathf.Abs(num) > 0.001f) { val.like = Mathf.Clamp01(val.like + num); report.Applied.Add("like " + ((num > 0f) ? "+" : "") + num.ToString("0.00")); if (Mathf.Abs(relationshipDelta.Like) > value + 0.001f) { report.Reject("like " + relationshipDelta.Like.ToString("0.00"), "capped at " + value.ToString("0.00")); } } float num2 = Mathf.Clamp(relationshipDelta.Known, 0f, value); if (num2 > 0.001f) { val.AddKnow(num2); report.Applied.Add("known +" + num2.ToString("0.00")); } } catch (Exception ex) { report.Reject("relationship", "threw: " + ex.Message); Plugin.Log.LogWarning((object)("Relationship update failed: " + ex.Message)); } } private static void ApplyAlarm(Citizen speaker, NpcReply reply, EffectReport report) { float value = ModConfig.MaxSuspicionShiftPerLine.Value; float num = Mathf.Clamp01(reply.Alarm); if (num <= 0.001f) { return; } try { if ((Object)(object)((Actor)speaker).ai == (Object)null) { return; } float alertness = ((Actor)speaker).ai.alertness; float num2 = Mathf.Clamp(num - alertness, 0f - value, value); if (!(Mathf.Abs(num2) < 0.01f)) { ((Actor)speaker).ai.alertness = Mathf.Clamp01(alertness + num2); if (num2 > 0f) { ((Actor)speaker).ai.TriggerReactionIndicator(); } report.Applied.Add("alertness " + ((num2 > 0f) ? "+" : "") + num2.ToString("0.00")); } } catch (Exception ex) { report.Reject("alarm", "threw: " + ex.Message); Plugin.Log.LogWarning((object)("Alarm update failed: " + ex.Message)); } } private static string ShiftAlertness(Citizen speaker, float delta) { if ((Object)(object)((Actor)speaker).ai == (Object)null) { return "no AI on this citizen"; } float value = ModConfig.MaxSuspicionShiftPerLine.Value; ((Actor)speaker).ai.alertness = Mathf.Clamp01(((Actor)speaker).ai.alertness + Mathf.Clamp(delta, 0f - value, value)); return null; } private static string EndConversation(Citizen speaker) { try { if ((Object)(object)((Actor)speaker).ai != (Object)null && ((Actor)speaker).ai.currentGoal != null) { ((Actor)speaker).ai.currentGoal.Complete(); } SpeechController speechController = ((Actor)speaker).speechController; if (speechController != null) { speechController.SetSpeechActive(false); } return null; } catch (Exception ex) { return "could not end the conversation: " + ex.Message; } } private static string Flee(Citizen speaker, string fromWhom) { if (!ModConfig.AllowCombatEffects.Value) { return "fleeing and combat are switched off"; } if ((Object)(object)((Actor)speaker).ai == (Object)null) { return "no AI on this citizen"; } if (((Actor)speaker).ai.restrained) { return "they are restrained and cannot run"; } ((Actor)speaker).ai.CancelCombat(); ((Actor)speaker).ai.inFleeState = true; ((Actor)speaker).ai.TriggerReactionIndicator(); if (!string.IsNullOrWhiteSpace(fromWhom)) { GoalDirector.Send(speaker, "go_home"); } return null; } private static string Attack(Citizen speaker, string targetName, bool shouted) { if (!ModConfig.AllowCombatEffects.Value) { return "fleeing and combat are switched off"; } if ((Object)(object)((Actor)speaker).ai == (Object)null) { return "no AI on this citizen"; } if (((Actor)speaker).ai.restrained) { return "they are restrained"; } Actor val = (Actor)(object)Player.Instance; if (!string.IsNullOrWhiteSpace(targetName)) { Citizen val2 = null; foreach (Citizen item in Earshot.CitizensWhoCanHear((Actor)(object)speaker, shouted)) { try { if (!((Object)(object)item == (Object)null) && !((Actor)item).isPlayer && ((Human)item).humanID != ((Human)speaker).humanID) { string citizenName = ((Human)item).GetCitizenName(); if (!string.IsNullOrEmpty(citizenName) && citizenName.IndexOf(targetName.Trim(), StringComparison.OrdinalIgnoreCase) >= 0) { val2 = item; break; } } } catch { } } if ((Object)(object)val2 == (Object)null) { return "that person is not here to be attacked"; } val = (Actor)(object)val2; } if ((Object)(object)val == (Object)null) { return "nobody to attack"; } ((Actor)speaker).ai.SetInCombat(true, false); ((Actor)speaker).ai.StartAttack(val); return null; } private static string Surrender(Citizen speaker) { if ((Object)(object)((Actor)speaker).ai == (Object)null) { return "no AI on this citizen"; } if (!((Actor)speaker).ai.inCombat && !((Actor)speaker).ai.inFleeState) { return "they were not fighting or fleeing"; } ((Actor)speaker).ai.CancelCombat(); ((Actor)speaker).ai.inFleeState = false; return null; } private static string GiveHeldItem(Citizen speaker) { if (!ModConfig.AllowItemHandover.Value) { return "handing over items is switched off"; } Player instance = Player.Instance; if ((Object)(object)instance == (Object)null) { return "no player to give to"; } Interactable val = ((Actor)speaker).rightHandInteractable ?? ((Actor)speaker).leftHandInteractable; if (val == null) { return "their hands are empty"; } return ((Human)instance).TryGiveItem(val, (Human)(object)speaker, true, true) ? null : "the game refused the handover"; } private static string AnswerDoor(Citizen speaker) { try { if ((Object)(object)((Actor)speaker).ai == (Object)null) { return "no AI on this citizen"; } if ((Object)(object)((Human)speaker).home == (Object)null) { return "they have no home"; } if (!((Actor)speaker).isHome) { return "they are not at home"; } List<NodeAccess> entrances = ((NewGameLocation)((Human)speaker).home).entrances; if (entrances == null || entrances.Count == 0) { return "their home has no entrance"; } NewDoor door = entrances[0].door; if ((Object)(object)door == (Object)null) { return "the entrance has no door"; } ((Actor)speaker).ai.AnswerDoor(door, ((Actor)speaker).currentGameLocation, (Actor)(object)Player.Instance); return null; } catch (Exception ex) { return "could not answer the door: " + ex.Message; } } private static List<Citizen> OfficersInEarshot(Citizen speaker, bool shouted) { List<Citizen> list = new List<Citizen>(); foreach (Citizen item in Earshot.CitizensWhoCanHear((Actor)(object)speaker, shouted)) { try { if ((Object)(object)item != (Object)null && ((Actor)item).isEnforcer && !((Actor)item).isDead) { list.Add(item); } } catch { } } return list; } private static string SetOfficerPursuit(Citizen speaker, Actor target, bool shouted) { if (!ModConfig.AllowPoliceRedirection.Value) { return "police redirection is switched off"; } if ((Object)(object)target == (Object)null) { return "nobody to pursue"; } List<Citizen> list = OfficersInEarshot(speaker, shouted); if (list.Count == 0) { return "no officer close enough to hear"; } bool flag = false; foreach (Citizen item in list) { try { ((Actor)item).ai.SetPersue(target, true, 2, true, 10f); flag = true; } catch { } } return flag ? null : "the officers refused the order"; } private static string CallOffOfficers(Citizen speaker, bool shouted) { if (!ModConfig.AllowPoliceRedirection.Value) { return "police redirection is switched off"; } List<Citizen> list = OfficersInEarshot(speaker, shouted); if (list.Count == 0) { return "no officer close enough to hear"; } bool flag = false; foreach (Citizen item in list) { try { if (!