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 Declared = new Dictionary(); 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 Export() { Dictionary dictionary = new Dictionary(); foreach (KeyValuePair item in Declared) { dictionary[item.Key.ToString()] = item.Value.ToString(); } return dictionary; } public static void Restore(Dictionary saved) { Declared.Clear(); if (saved == null) { return; } foreach (KeyValuePair 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 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 LastSeen = new Dictionary(); private static readonly HashSet WasNear = new HashSet(); private static string _playerHeld = ""; private static bool _playerArmed; private static bool _watchingPlayerStarted; private static readonly Queue Pending = new Queue(); 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 hashSet = new HashSet(); 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(); 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 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 Audience(Citizen speaker, bool shouted) { List list = new List(); 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 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 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 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 PossibleDetails(Citizen citizen) { List list = new List(); 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 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 list = new List(); 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 Result; } private static readonly Dictionary Recent = new Dictionary(); 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 CitizensWhoCanHear(Actor origin, VoiceLevel level) { return Gather(origin, Voice.RadiusOf(level), Voice.CarriesNextDoor(level)); } public static List CitizensWhoCanHear(Actor origin, bool shouted) { return Cached(origin, Radius(shouted), shouted); } private static List Cached(Actor origin, float radius, bool shouted) { if ((Object)(object)origin == (Object)null) { return new List(); } 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 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 list = new List(); foreach (KeyValuePair 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 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 list = new List(); if ((Object)(object)origin == (Object)null) { return list; } Vector3 originPos = (((Object)(object)((Component)origin).transform != (Object)null) ? ((Component)origin).transform.position : Vector3.zero); HashSet hashSet = new HashSet(); try { if ((Object)(object)origin.currentRoom != (Object)null) { hashSet.Add(origin.currentRoom); if (shouted && origin.currentRoom.adjacentRooms != null) { Enumerator 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 hashSet2 = new HashSet(); foreach (NewRoom item in hashSet) { HashSet val = null; try { val = item.currentOccupants; } catch { } if (val == null) { continue; } Enumerator 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 val3 = (((Object)(object)CityData.Instance != (Object)null) ? CityData.Instance.citizenDirectory : null); if (val3 != null) { Enumerator 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(); } 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 Enabled = () => true; public Func Run; public string[] Aliases = new string[0]; public Func 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 All = new List(); private static readonly Dictionary ByName = new Dictionary(); 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 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 list = new List(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 Following = new Dictionary(); 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(); } catch { return null; } } public static bool IsFollowing(Citizen c) { return (Object)(object)c != (Object)null && Following.ContainsKey(((Human)c).humanID); } public static List Names() { List list = new List(); 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 list = null; foreach (KeyValuePair 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())).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())).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 Intents = new Dictionary { { "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 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 val = (((Object)(object)Toolbox.Instance != (Object)null) ? Toolbox.Instance.allGoals : null); if (val == null) { return null; } foreach (string value in keywords) { Enumerator 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 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 list = new List(); Enumerator 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 list2 = new List(); foreach (KeyValuePair 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 Outstanding = new Dictionary(); 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 walletItems = ((Human)citizen).walletItems; if (walletItems != null) { bool flag = false; Enumerator 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 Export() { Dictionary dictionary = new Dictionary(); foreach (KeyValuePair 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 saved) { Outstanding.Clear(); if (saved == null) { return; } foreach (KeyValuePair 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 KnownPeople(Citizen speaker, int max = 6) { List list = new List(); if ((Object)(object)speaker == (Object)null) { return list; } try { if (((Human)speaker).acquaintances == null) { return list; } Enumerator 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 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() : 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 PossibleSubjects(Citizen witness, int max = 6) { List list = new List(); if ((Object)(object)witness == (Object)null) { return list; } try { if (((Human)witness).lastSightings == null) { return list; } Enumerator enumerator = ((Human)witness).lastSightings.GetEnumerator(); while (enumerator.MoveNext()) { KeyValuePair 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 enumerator = ((Human)witness).lastSightings.GetEnumerator(); while (enumerator.MoveNext()) { KeyValuePair 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 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 list = new List(); if ((Object)(object)citizen == (Object)null) { return list; } try { List walletItems = ((Human)citizen).walletItems; if (walletItems == null) { return list; } int num = 0; int num2 = 0; int num3 = 0; Enumerator 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 val = ((citizen != null) ? ((Human)citizen).walletItems : null); if (val == null) { return 0; } Enumerator 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 walletItems = ((Human)citizen).walletItems; if (walletItems == null) { return false; } Enumerator 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 Applied = new List(); public readonly List Rejected = new List(); 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 run, Func gate = null, string conflicts = null, string[] aliases = null, Func relevant = null, bool needsInvitation = false) { EffectCatalogue.Definition definition = new EffectCatalogue.Definition(); definition.Name = name; definition.Description = description; definition.Run = run; definition.Enabled = gate ?? ((Func)(() => true)); definition.Conflicts = conflicts; definition.Aliases = aliases ?? new string[0]; definition.Relevant = relevant; definition.NeedsInvitation = needsInvitation; EffectCatalogue.Register(definition); } public static IEnumerable 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 hashSet = new HashSet(); Dictionary dictionary = new Dictionary(); 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 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 OfficersInEarshot(Citizen speaker, bool shouted) { List list = new List(); 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 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 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 (!((Object)(object)((Actor)item).ai == (Object)null) && ((Actor)item).ai.persuit) { ((Actor)item).ai.CancelPersue(); flag = true; } } catch { } } return flag ? null : "no officer was chasing anyone"; } private static string AccuseOther(Citizen speaker, string targetName, bool shouted) { if (!ModConfig.AllowPoliceRedirection.Value) { return "police redirection is switched off"; } if (string.IsNullOrWhiteSpace(targetName)) { return "no name given to accuse"; } Citizen val = null; foreach (Citizen item in Earshot.CitizensWhoCanHear((Actor)(object)speaker, shouted)) { try { if (!((Object)(object)item == (Object)null) && !((Actor)item).isPlayer) { string citizenName = ((Human)item).GetCitizenName(); if (!string.IsNullOrEmpty(citizenName) && citizenName.IndexOf(targetName.Trim(), StringComparison.OrdinalIgnoreCase) >= 0) { val = item; break; } } } catch { } } if ((Object)(object)val == (Object)null) { return "that person is not here to be accused"; } return SetOfficerPursuit(speaker, (Actor)(object)val, shouted); } } } namespace LooseLips.Player2 { public static class Player2Client { private readonly struct Attempt { public readonly NpcReply Reply; public readonly bool Delivered; public Attempt(NpcReply reply, bool delivered) { Reply = reply; Delivered = delivered; } } private static readonly HttpClient Http = new HttpClient(); private static readonly JsonSerializerOptions JsonOpts = new JsonSerializerOptions { PropertyNameCaseInsensitive = true, DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull }; private static Timer _heartbeat; private static readonly TimeSpan RetryWindow = TimeSpan.FromSeconds(8.0); public static bool Available { get; private set; } public static string LastError { get; private set; } public static void Initialise() { Http.Timeout = TimeSpan.FromSeconds(ModConfig.RequestTimeoutSeconds.Value); Http.DefaultRequestHeaders.Remove("player2-game-key"); Http.DefaultRequestHeaders.Add("player2-game-key", ModConfig.GameKey.Value); _heartbeat = new Timer(delegate(object? _) { _ = ProbeAsync(); }, null, TimeSpan.Zero, TimeSpan.FromSeconds(60.0)); } public static void Shutdown() { _heartbeat?.Dispose(); _heartbeat = null; } private static string Url(string path) { string text = ModConfig.BaseUrl.Value.TrimEnd('/'); if (!path.StartsWith("/")) { path = "/" + path; } return text + path; } public static async Task ReadBalanceAsync() { try { using HttpResponseMessage resp = await Http.GetAsync(Url("/v1/joules")).ConfigureAwait(continueOnCapturedContext: false); if (!resp.IsSuccessStatusCode) { Player2Status.Saw((int)resp.StatusCode); return; } JouleReading reading = JsonSerializer.Deserialize(await resp.Content.ReadAsStringAsync().ConfigureAwait(continueOnCapturedContext: false), JsonOpts); if (reading != null) { Player2Status.Reading(reading.Joules, reading.Tier); } } catch { } } public static async Task ProbeAsync() { try { using HttpResponseMessage resp = await Http.GetAsync(Url(ModConfig.HealthPath.Value)).ConfigureAwait(continueOnCapturedContext: false); bool ok = resp.IsSuccessStatusCode; if (ok) { await ReadBalanceAsync().ConfigureAwait(continueOnCapturedContext: false); } else { Player2Status.Saw((int)resp.StatusCode); } if (ok != Available) { string msg = (ok ? "Player2 app is reachable." : ("Player2 health check returned " + (int)resp.StatusCode + ".")); MainThread.Post(delegate { Plugin.Log.LogInfo((object)msg); }); } Available = ok; return ok; } catch (Exception ex) { Exception e = ex; if (Available) { MainThread.Post(delegate { Plugin.Log.LogWarning((object)"Lost contact with the Player2 app. Free-form dialogue will fall back to vanilla lines."); }); } Available = false; Player2Status.Unreachable(); LastError = e.Message; return false; } } public static async Task GenerateReplyAsync(string systemPrompt, IReadOnlyList history, string userTurn, CancellationToken ct = default(CancellationToken), bool retryIfUnusable = false) { ChatRequest req = new ChatRequest { Model = (string.IsNullOrWhiteSpace(ModConfig.Model.Value) ? null : ModConfig.Model.Value), Temperature = 0.85f, MaxTokens = 400 }; req.Messages.Add(ChatMessage.System(systemPrompt)); if (history != null) { req.Messages.AddRange(history); } req.Messages.Add(ChatMessage.User(userTurn)); Attempt attempt = await AttemptAsync(req, systemPrompt, userTurn, ct).ConfigureAwait(continueOnCapturedContext: false); if (!retryIfUnusable || !attempt.Delivered || Usable(attempt.Reply)) { return attempt.Reply; } Stopwatch spent = Stopwatch.StartNew(); NpcReply firstFailure = attempt.Reply; for (int retry = 0; retry < 2; retry++) { if (!(spent.Elapsed < RetryWindow)) { break; } MainThread.Post(delegate { Plugin.Log.LogInfo((object)"The model returned nothing the citizen could say. Asking again."); }); attempt = await AttemptAsync(req, systemPrompt, userTurn, ct).ConfigureAwait(continueOnCapturedContext: false); if (Usable(attempt.Reply)) { return attempt.Reply; } if (!attempt.Delivered) { break; } } return firstFailure; } private static bool Usable(NpcReply reply) { return reply != null && !string.IsNullOrWhiteSpace(reply.Speech); } private static async Task AttemptAsync(ChatRequest req, string systemPrompt, string userTurn, CancellationToken ct) { Stopwatch clock = Stopwatch.StartNew(); string raw; try { string body = JsonSerializer.Serialize(req, JsonOpts); if (ModConfig.LogPrompts.Value) { MainThread.Post(delegate { Plugin.Log.LogInfo((object)("[prompt] " + systemPrompt + "\n[turn] " + userTurn)); }); } using StringContent content = new StringContent(body, Encoding.UTF8, "application/json"); using HttpResponseMessage resp = await Http.PostAsync(Url(ModConfig.ChatPath.Value), content, ct).ConfigureAwait(continueOnCapturedContext: false); Player2Status.Saw((int)resp.StatusCode); if (!resp.IsSuccessStatusCode) { LastError = string.Concat(str2: Truncate(await resp.Content.ReadAsStringAsync().ConfigureAwait(continueOnCapturedContext: false), 400), str0: ((int)resp.StatusCode).ToString(), str1: ": "); string captured = LastError; MainThread.Post(delegate { Plugin.Log.LogWarning((object)("Player2 chat request failed - " + captured)); }); return new Attempt(Failed(clock, captured), delivered: false); } ChatResponse parsed = JsonSerializer.Deserialize(await resp.Content.ReadAsStringAsync().ConfigureAwait(continueOnCapturedContext: false), JsonOpts); raw = ((parsed?.Choices == null || parsed.Choices.Count <= 0) ? null : parsed.Choices[0].Message?.Content); if (ModConfig.VerboseLogging.Value) { string shown = Truncate(raw, 800); MainThread.Post(delegate { Plugin.Log.LogInfo((object)("[raw reply] " + shown)); }); } } catch (OperationCanceledException) { return new Attempt(Failed(clock, "timed out after " + ModConfig.RequestTimeoutSeconds.Value + " s"), delivered: false); } catch (Exception ex2) { Exception ex3 = ex2; Exception e = ex3; LastError = e.Message; MainThread.Post(delegate { Plugin.Log.LogWarning((object)("Player2 chat request threw: " + e.Message)); }); return new Attempt(Failed(clock, e.Message), delivered: false); } clock.Stop(); NpcReply parsed2 = ParseReply(raw); if (parsed2 != null) { parsed2.LatencyMs = clock.ElapsedMilliseconds; } return new Attempt(parsed2, delivered: true); } private static NpcReply Failed(Stopwatch clock, string reason) { clock.Stop(); return new NpcReply { Speech = null, Raw = reason, WellFormed = false, LatencyMs = clock.ElapsedMilliseconds }; } public static NpcReply ParseReply(string raw) { if (string.IsNullOrWhiteSpace(raw)) { return null; } string text = ExtractJsonObject(raw); if (text != null) { try { NpcReply npcReply = JsonSerializer.Deserialize(text, JsonOpts); if (npcReply != null && !string.IsNullOrWhiteSpace(npcReply.Speech)) { npcReply.Speech = Sanitise(npcReply.Speech); npcReply.Raw = raw; npcReply.WellFormed = true; return npcReply; } } catch (JsonException) { } } string text2 = ReplySalvage.SpeechFromPartialJson(raw); if (text2 != null) { return new NpcReply { Speech = Sanitise(text2), Truthfulness = 1f, Raw = raw, WellFormed = false }; } if (ReplySalvage.LooksLikeMachineOutput(raw)) { return new NpcReply { Speech = null, Raw = raw, WellFormed = false }; } return new NpcReply { Speech = Sanitise(raw), Truthfulness = 1f, Raw = raw, WellFormed = false }; } public static NpcExchange ParseExchange(string raw) { if (string.IsNullOrWhiteSpace(raw)) { return null; } string text = ExtractJsonObject(raw); if (text == null) { return null; } try { NpcExchange npcExchange = JsonSerializer.Deserialize(text, JsonOpts); if (npcExchange?.Lines == null) { return null; } foreach (ExchangeLine line in npcExchange.Lines) { if (line != null) { line.Says = Sanitise(line.Says); } } return npcExchange; } catch (JsonException) { return null; } } private static string ExtractJsonObject(string s) { Match match = Regex.Match(s, "```(?:json)?\\s*(\\{[\\s\\S]*?\\})\\s*```", RegexOptions.IgnoreCase); if (match.Success) { return match.Groups[1].Value; } int num = s.IndexOf('{'); if (num < 0) { return null; } int num2 = 0; bool flag = false; bool flag2 = false; for (int i = num; i < s.Length; i++) { char c = s[i]; if (flag) { if (flag2) { flag2 = false; continue; } switch (c) { case '\\': flag2 = true; break; case '"': flag = false; break; } continue; } switch (c) { case '"': flag = true; break; case '{': num2++; break; case '}': num2--; if (num2 == 0) { return s.Substring(num, i - num + 1); } break; } } return null; } private static string Sanitise(string s) { if (string.IsNullOrWhiteSpace(s)) { return s; } s = s.Trim(); s = Regex.Replace(s, "^```(?:json)?|```$", "").Trim(); s = Regex.Replace(s, "\\s+", " "); s = s.Replace("|", "/").Replace("<", "(").Replace(">", ")"); s = FoldPunctuation(s); int value = ModConfig.MaxReplyCharacters.Value; if (s.Length > value) { int num = s.LastIndexOfAny(new char[3] { '.', '!', '?' }, Math.Min(value, s.Length - 1)); s = ((num > value / 2) ? s.Substring(0, num + 1) : (s.Substring(0, value).TrimEnd() + "...")); } return s; } private static string FoldPunctuation(string s) { if (string.IsNullOrEmpty(s)) { return s; } StringBuilder stringBuilder = new StringBuilder(s.Length); foreach (char c in s) { switch (c) { case '–': case '—': case '−': stringBuilder.Append('-'); break; case '‘': case '’': stringBuilder.Append('\''); break; case '“': case '”': stringBuilder.Append('"'); break; case '…': stringBuilder.Append("..."); break; case '\u00a0': stringBuilder.Append(' '); break; default: stringBuilder.Append(c); break; } } return stringBuilder.ToString().Trim('"'); } private static string Truncate(string s, int n) { if (string.IsNullOrEmpty(s)) { return s; } return (s.Length <= n) ? s : (s.Substring(0, n) + "..."); } public static async Task SpeakAsync(string text, string voiceId = null) { if (!ModConfig.EnableTts.Value || string.IsNullOrWhiteSpace(text)) { return; } try { TtsRequest req = new TtsRequest { Text = text, PlayInApp = true, Speed = ModConfig.TtsSpeed.Value }; if (!string.IsNullOrWhiteSpace(voiceId)) { req.VoiceIds = new List { voiceId }; } string body = JsonSerializer.Serialize(req, JsonOpts); using StringContent content = new StringContent(body, Encoding.UTF8, "application/json"); using CancellationTokenSource deadline = new CancellationTokenSource(TimeSpan.FromSeconds(ModConfig.TtsTimeoutSeconds.Value)); Stopwatch clock = Stopwatch.StartNew(); using HttpResponseMessage resp = await Http.PostAsync(Url(ModConfig.TtsPath.Value), content, deadline.Token).ConfigureAwait(continueOnCapturedContext: false); clock.Stop(); if (ModConfig.VerboseLogging.Value) { long ms = clock.ElapsedMilliseconds; MainThread.Post(delegate { Plugin.Log.LogInfo((object)("Spoken line synthesised in " + ms + " ms.")); }); } if (!resp.IsSuccessStatusCode) { int code = (int)resp.StatusCode; string detail = Truncate(await resp.Content.ReadAsStringAsync().ConfigureAwait(continueOnCapturedContext: false), 300); MainThread.Post(delegate { Plugin.Log.LogWarning((object)("TTS request returned " + code + ": " + detail)); }); } } catch (Exception ex) { Exception ex2 = ex; Exception e = ex2; if (ModConfig.VerboseLogging.Value) { MainThread.Post(delegate { Plugin.Log.LogWarning((object)("TTS request threw: " + e.Message)); }); } } } } public sealed class ChatMessage { [JsonPropertyName("role")] public string Role { get; set; } [JsonPropertyName("content")] public string Content { get; set; } public static ChatMessage System(string c) { return new ChatMessage { Role = "system", Content = c }; } public static ChatMessage User(string c) { return new ChatMessage { Role = "user", Content = c }; } public static ChatMessage Assistant(string c) { return new ChatMessage { Role = "assistant", Content = c }; } } public sealed class ChatRequest { [JsonPropertyName("model")] public string Model { get; set; } [JsonPropertyName("messages")] public List Messages { get; set; } = new List(); [JsonPropertyName("temperature")] public float Temperature { get; set; } = 0.85f; [JsonPropertyName("max_tokens")] public int MaxTokens { get; set; } = 400; } public sealed class ChatChoice { [JsonPropertyName("message")] public ChatMessage Message { get; set; } } public sealed class ChatResponse { [JsonPropertyName("choices")] public List Choices { get; set; } } public sealed class TtsRequest { [JsonPropertyName("text")] public string Text { get; set; } [JsonPropertyName("play_in_app")] public bool PlayInApp { get; set; } = true; [JsonPropertyName("speed")] public float Speed { get; set; } = 1f; [JsonPropertyName("voice_ids")] public List VoiceIds { get; set; } } public sealed class JouleReading { [JsonPropertyName("joules")] public int Joules { get; set; } [JsonPropertyName("patron_tier")] public string Tier { get; set; } } public sealed class Voice { [JsonPropertyName("id")] public string Id { get; set; } [JsonPropertyName("name")] public string Name { get; set; } [JsonPropertyName("language")] public string Language { get; set; } [JsonPropertyName("gender")] public string Gender { get; set; } } public sealed class VoiceList { [JsonPropertyName("voices")] public List Voices { get; set; } } public sealed class WorldEffect { [JsonPropertyName("type")] public string Type { get; set; } [JsonPropertyName("target")] public string Target { get; set; } [JsonPropertyName("detail")] public string Detail { get; set; } } public sealed class RelationshipDelta { [JsonPropertyName("like")] [JsonConverter(typeof(TolerantJson.FlexibleFloat))] public float Like { get; set; } [JsonPropertyName("known")] public float Known { get; set; } [JsonPropertyName("suspicion")] public float Suspicion { get; set; } } public sealed class NpcReply { [JsonPropertyName("reason")] public string Reason { get; set; } [JsonPropertyName("speech")] public string Speech { get; set; } [JsonPropertyName("truthfulness")] [JsonConverter(typeof(TolerantJson.FlexibleFloat))] public float Truthfulness { get; set; } = 1f; [JsonPropertyName("voice")] public string Voice { get; set; } [JsonPropertyName("alarm")] [JsonConverter(typeof(TolerantJson.FlexibleFloat))] public float Alarm { get; set; } [JsonPropertyName("effects")] [JsonConverter(typeof(TolerantJson.FlexibleEffectList))] public List Effects { get; set; } = new List(); [JsonPropertyName("relationship_delta")] [JsonConverter(typeof(TolerantJson.FlexibleRelationship))] public RelationshipDelta RelationshipDelta { get; set; } [JsonIgnore] public string Raw { get; set; } [JsonIgnore] public bool WellFormed { get; set; } [JsonIgnore] public long LatencyMs { get; set; } } public sealed class ExchangeLine { [JsonPropertyName("who")] public string Who { get; set; } [JsonPropertyName("says")] public string Says { get; set; } } public sealed class GossipItem { [JsonPropertyName("teller")] public string Teller { get; set; } [JsonPropertyName("about")] public string About { get; set; } } public sealed class NpcExchange { [JsonPropertyName("lines")] public List Lines { get; set; } [JsonPropertyName("gossip")] public GossipItem Gossip { get; set; } } public static class Player2Status { public enum State { Unknown, Fine, NotSignedIn, OutOfCredits, RateLimited, Unreachable } private static int Consecutive429; public static State Current { get; private set; } = State.Unknown; public static int Joules { get; private set; } = -1; public static string Tier { get; private set; } = ""; public static float QuietUntil { get; private set; } public static bool ShouldHoldBackAmbient => Current == State.OutOfCredits || Current == State.RateLimited || Current == State.NotSignedIn || Time.time < QuietUntil || (Joules >= 0 && Joules < ModConfig.MinJoulesForAmbient.Value); public static void Reading(int joules, string tier) { Joules = joules; Tier = tier ?? ""; if (Current == State.Unknown || Current == State.Unreachable) { Current = State.Fine; } } public static void Saw(int statusCode) { switch (statusCode) { case 401: Set(State.NotSignedIn, 300f, "Player2 says you are not signed in. Open the Player2 app and log in."); return; case 402: Set(State.OutOfCredits, 600f, "Player2 is out of credits. Free-form dialogue will pause until the balance recovers; background chatter stays off in the meantime."); return; case 429: { float num = Mathf.Min(60f * (float)(1 + Consecutive429), 600f); Consecutive429++; Set(State.RateLimited, num, "Player2 is rate limiting us. Holding back for " + Mathf.RoundToInt(num) + " s."); return; } } if (statusCode >= 200 && statusCode < 300) { Consecutive429 = 0; if (Current != State.Fine) { Current = State.Fine; Plugin.Log.LogInfo((object)"Player2 is answering normally again."); } } } public static void Unreachable() { Current = State.Unreachable; } private static void Set(State state, float quietSeconds, string message) { bool flag = Current != state; Current = state; QuietUntil = Time.time + quietSeconds; if (flag) { Plugin.Log.LogWarning((object)message); } } public static string Describe() { string text = ((Joules >= 0) ? (Joules + " credits" + (string.IsNullOrEmpty(Tier) ? "" : (" on " + Tier))) : "credits unknown"); return Current switch { State.NotSignedIn => "Not signed in to Player2.", State.OutOfCredits => "Out of Player2 credits. " + text, State.RateLimited => "Rate limited by Player2. " + text, State.Unreachable => "Cannot reach the Player2 app.", State.Fine => text, _ => "Not checked yet.", }; } public static void Reset() { Current = State.Unknown; Joules = -1; Tier = ""; QuietUntil = 0f; Consecutive429 = 0; } } public static class ReplySalvage { private static readonly Regex OurKeys = new Regex("\"(speech|reason|effects|truthfulness|alarm|relationship_delta)\"\\s*:", RegexOptions.IgnoreCase | RegexOptions.Compiled); private static readonly Regex WholeSpeech = new Regex("\"speech\"\\s*:\\s*\"((?:[^\"\\\\]|\\\\.)*)\"", RegexOptions.IgnoreCase | RegexOptions.Compiled); private static readonly Regex StartedSpeech = new Regex("\"speech\"\\s*:\\s*\"((?:[^\"\\\\]|\\\\.)*)$", RegexOptions.IgnoreCase | RegexOptions.Compiled); public static bool LooksLikeMachineOutput(string text) { if (string.IsNullOrWhiteSpace(text)) { return false; } string text2 = text.TrimStart(); if (text2.StartsWith("{") || text2.StartsWith("[") || text2.StartsWith("```")) { return true; } return OurKeys.IsMatch(text); } public static string SpeechFromPartialJson(string text) { if (string.IsNullOrWhiteSpace(text)) { return null; } Match match = WholeSpeech.Match(text); if (match.Success) { return Clean(Unescape(match.Groups[1].Value)); } Match match2 = StartedSpeech.Match(text); if (!match2.Success) { return null; } string text2 = Unescape(match2.Groups[1].Value); int num = text2.LastIndexOfAny(new char[3] { '.', '!', '?' }); return (num < 0) ? null : Clean(text2.Substring(0, num + 1)); } private static string Unescape(string value) { if (string.IsNullOrEmpty(value)) { return value; } return value.Replace("\\\"", "\"").Replace("\\n", " ").Replace("\\r", " ") .Replace("\\t", " ") .Replace("\\\\", "\\"); } private static string Clean(string value) { if (value == null) { return null; } value = value.Trim(); return (value.Length == 0) ? null : value; } } internal static class TolerantJson { internal sealed class FlexibleFloat : JsonConverter { public override float Read(ref Utf8JsonReader reader, Type type, JsonSerializerOptions options) { switch (reader.TokenType) { case JsonTokenType.Number: return Scale(reader.GetSingle()); case JsonTokenType.String: { string text = reader.GetString(); if (float.TryParse(text, NumberStyles.Float, CultureInfo.InvariantCulture, out var result)) { return Scale(result); } return WordToNumber(text); } case JsonTokenType.True: return 1f; case JsonTokenType.False: return 0f; case JsonTokenType.Null: return 0f; default: reader.Skip(); return 0f; } } private static float Scale(float v) { return (v > 1f && v <= 100f) ? (v / 100f) : v; } private static float WordToNumber(string text) { if (string.IsNullOrWhiteSpace(text)) { return 0f; } switch (text.Trim().ToLowerInvariant()) { case "none": case "no": case "never": return 0f; case "low": case "slight": case "a little": return 0.25f; case "medium": case "moderate": case "some": return 0.5f; case "high": case "very": case "a lot": return 0.75f; case "full": case "total": case "complete": case "yes": return 1f; default: return 0f; } } public override void Write(Utf8JsonWriter writer, float value, JsonSerializerOptions options) { writer.WriteNumberValue(value); } } internal sealed class FlexibleEffectList : JsonConverter> { public override bool HandleNull => true; public override List Read(ref Utf8JsonReader reader, Type type, JsonSerializerOptions options) { List list = new List(); switch (reader.TokenType) { case JsonTokenType.Null: return list; case JsonTokenType.String: Add(list, reader.GetString()); return list; case JsonTokenType.StartObject: { WorldEffect worldEffect2 = ReadOne(ref reader, options); if (worldEffect2 != null) { list.Add(worldEffect2); } return list; } case JsonTokenType.StartArray: while (reader.Read() && reader.TokenType != JsonTokenType.EndArray) { if (reader.TokenType == JsonTokenType.String) { Add(list, reader.GetString()); } else if (reader.TokenType == JsonTokenType.StartObject) { WorldEffect worldEffect = ReadOne(ref reader, options); if (worldEffect != null) { list.Add(worldEffect); } } else { reader.Skip(); } } return list; default: reader.Skip(); return list; } } private static void Add(List list, string name) { if (!string.IsNullOrWhiteSpace(name)) { list.Add(new WorldEffect { Type = name }); } } private static WorldEffect ReadOne(ref Utf8JsonReader reader, JsonSerializerOptions options) { WorldEffect worldEffect = new WorldEffect(); while (reader.Read() && reader.TokenType != JsonTokenType.EndObject) { if (reader.TokenType != JsonTokenType.PropertyName) { reader.Skip(); continue; } string text = reader.GetString(); if (!reader.Read()) { break; } string text2 = ReadScalar(ref reader); if (string.IsNullOrEmpty(text)) { continue; } switch (text.ToLowerInvariant()) { case "type": case "effect": case "name": case "action": worldEffect.Type = text2; break; case "target": case "who": case "person": case "amount": if (string.IsNullOrEmpty(worldEffect.Target)) { worldEffect.Target = text2; } break; case "detail": case "details": case "reason": case "for": worldEffect.Detail = text2; break; } } return string.IsNullOrWhiteSpace(worldEffect.Type) ? null : worldEffect; } private static string ReadScalar(ref Utf8JsonReader reader) { switch (reader.TokenType) { case JsonTokenType.String: return reader.GetString(); case JsonTokenType.Number: { double value; return reader.TryGetDouble(out value) ? value.ToString(CultureInfo.InvariantCulture) : null; } case JsonTokenType.True: return "true"; case JsonTokenType.False: return "false"; case JsonTokenType.Null: return null; default: reader.Skip(); return null; } } public override void Write(Utf8JsonWriter writer, List value, JsonSerializerOptions options) { JsonSerializer.Serialize(writer, value, options); } } internal sealed class FlexibleRelationship : JsonConverter { public override RelationshipDelta Read(ref Utf8JsonReader reader, Type type, JsonSerializerOptions options) { if (reader.TokenType == JsonTokenType.Null) { return null; } if (reader.TokenType == JsonTokenType.Number) { return new RelationshipDelta { Like = reader.GetSingle() }; } if (reader.TokenType != JsonTokenType.StartObject) { reader.Skip(); return null; } RelationshipDelta relationshipDelta = new RelationshipDelta(); FlexibleFloat flexibleFloat = new FlexibleFloat(); while (reader.Read() && reader.TokenType != JsonTokenType.EndObject) { if (reader.TokenType != JsonTokenType.PropertyName) { reader.Skip(); continue; } string text = reader.GetString(); if (!reader.Read()) { break; } float num = flexibleFloat.Read(ref reader, typeof(float), options); if (!string.IsNullOrEmpty(text)) { switch (text.ToLowerInvariant()) { case "like": case "liking": case "affection": relationshipDelta.Like = num; break; case "known": case "know": case "familiarity": relationshipDelta.Known = num; break; case "suspicion": case "suspicious": relationshipDelta.Suspicion = num; break; } } } return relationshipDelta; } public override void Write(Utf8JsonWriter writer, RelationshipDelta value, JsonSerializerOptions options) { JsonSerializer.Serialize(writer, value, options); } } } } namespace LooseLips.Dialog { public sealed class ChatOverlay : MonoBehaviour { private static ChatOverlay _instance; private static bool _open; private static int _targetId; private static bool _shouted; private static Action _onSubmit; private static string _text = string.Empty; private static bool _focusRequested; private const int WindowWidth = 620; private const int WindowHeight = 132; private const string ControlName = "LooseLipsChatInput"; private static bool _registered; public ChatOverlay(IntPtr ptr) : base(ptr) { } public static void Install() { //IL_0036: Unknown result type (might be due to invalid IL or missing references) //IL_003c: Expected O, but got Unknown //IL_0066: 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) if ((Object)(object)_instance != (Object)null) { return; } try { if (!_registered) { ClassInjector.RegisterTypeInIl2Cpp(); _registered = true; } GameObject val = new GameObject("LooseLips.ChatOverlay"); Object.DontDestroyOnLoad((Object)(object)val); ((Object)val).hideFlags = (HideFlags)61; _instance = val.AddComponent(); Plugin.Log.LogInfo((object)("Chat overlay installed. Press " + ((object)ModConfig.SettingsHotkey.Value/*cast due to .constrained prefix*/).ToString() + " for settings.")); } catch (Exception ex) { Plugin.Log.LogError((object)("Could not install the chat overlay: " + ex)); } } public static void Open(Citizen target, bool shouted, Action onSubmit) { GameInput.Claim(); _targetId = (((Object)(object)target != (Object)null) ? ((Human)target).humanID : 0); _shouted = shouted; _onSubmit = onSubmit; _text = string.Empty; _open = true; _focusRequested = true; } public static void Close() { _open = false; if (!SettingsWindow.IsOpen) { GameInput.Release(); } _targetId = 0; _onSubmit = null; _text = string.Empty; } private void Update() { //IL_0066: Unknown result type (might be due to invalid IL or missing references) MainThread.Drain(); ConversationMemory.EnsureLoaded(); ConversationMemory.Flush(); WorldMemory.EnsureLoaded(); DelayedSpeech.Tick(); NpcConversation.Tick(); FollowDirector.Tick(); Allegiance.DefendPlayer(); AmbientReactions.Tick(); if (SettingsWindow.IsOpen || _open) { GameInput.Tick(); } else if (GameInput.Held) { GameInput.Release(); } if (Input.GetKeyDown(ModConfig.SettingsHotkey.Value)) { Plugin.Log.LogInfo((object)"Settings hotkey pressed."); if (_open) { Close(); } SettingsWindow.Toggle(); } else if (Input.GetKeyDown((KeyCode)27)) { if (SettingsWindow.IsOpen) { SettingsWindow.Close(); } else if (_open) { Close(); } } } private void OnGUI() { GUI.depth = -1000; SettingsWindow.Draw(); if (!SettingsWindow.IsOpen) { if (_open) { DrawInputBox(); } else if (ModConfig.ShowVoiceReachMeter.Value) { DrawReachMeter(); } } } private void DrawInputBox() { //IL_000b: Unknown result type (might be due to invalid IL or missing references) //IL_0011: Invalid comparison between Unknown and I4 //IL_0014: Unknown result type (might be due to invalid IL or missing references) //IL_001b: Invalid comparison between Unknown and I4 //IL_0063: Unknown result type (might be due to invalid IL or missing references) //IL_0068: 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_006e: Unknown result type (might be due to invalid IL or missing references) //IL_007a: Unknown result type (might be due to invalid IL or missing references) //IL_007f: Unknown result type (might be due to invalid IL or missing references) //IL_001e: Unknown result type (might be due to invalid IL or missing references) //IL_0028: Invalid comparison between Unknown and I4 //IL_00fc: Unknown result type (might be due to invalid IL or missing references) //IL_0107: Unknown result type (might be due to invalid IL or missing references) //IL_0114: Unknown result type (might be due to invalid IL or missing references) //IL_0138: Unknown result type (might be due to invalid IL or missing references) //IL_02b6: Unknown result type (might be due to invalid IL or missing references) //IL_027f: Unknown result type (might be due to invalid IL or missing references) //IL_0296: Unknown result type (might be due to invalid IL or missing references) Event current = Event.current; if (current != null && (int)current.type == 4 && ((int)current.keyCode == 13 || (int)current.keyCode == 271)) { Submit(); current.Use(); return; } float num = Mathf.Clamp(ModConfig.UiScale.Value, 0.6f, 2.5f); Matrix4x4 matrix = GUI.matrix; GUI.matrix = Matrix4x4.TRS(Vector3.zero, Quaternion.identity, new Vector3(num, num, 1f)); float num2 = ((float)Screen.width / num - 620f) / 2f; float num3 = (float)Screen.height / num - 132f - 90f; Rect val = default(Rect); ((Rect)(ref val))..ctor(num2, num3, 620f, 132f); Skin.Scope scope = (ModConfig.TintTheChatBox.Value ? Skin.Begin() : default(Skin.Scope)); GUI.color = new Color(0f, 0f, 0f, 0.82f); GUI.Box(val, GUIContent.none); GUI.color = Color.white; GUILayout.BeginArea(new Rect(num2 + 14f, num3 + 10f, 592f, 112f)); string text = SafeName(Humans.Resolve(_targetId)); string text2 = (_shouted ? ("Shout at " + text) : ("Say to " + text)); int num4 = CountListeners(); if (num4 > 0) { text2 = text2 + " (" + num4 + " other" + ((num4 == 1) ? "" : "s") + " within earshot)"; } GUILayout.Label(text2, (Il2CppReferenceArray)null); GUI.SetNextControlName("LooseLipsChatInput"); _text = GUILayout.TextField(_text ?? string.Empty, 300, (GUILayoutOption[])(object)new GUILayoutOption[1] { GUILayout.Height(26f) }); if (_focusRequested) { GUI.FocusControl("LooseLipsChatInput"); if (GUI.GetNameOfFocusedControl() == "LooseLipsChatInput") { _focusRequested = false; } } GUILayout.BeginHorizontal((Il2CppReferenceArray)null); GUILayout.Label("Enter to speak, Escape to cancel.", (Il2CppReferenceArray)null); GUILayout.FlexibleSpace(); if (!Player2Client.Available) { GUI.color = new Color(1f, 0.6f, 0.4f); GUILayout.Label("Player2 app not detected", (Il2CppReferenceArray)null); GUI.color = Color.white; } GUILayout.EndHorizontal(); GUILayout.EndArea(); scope.End(); GUI.matrix = matrix; } private void DrawReachMeter() { //IL_008a: Unknown result type (might be due to invalid IL or missing references) //IL_0095: Unknown result type (might be due to invalid IL or missing references) //IL_00a1: Unknown result type (might be due to invalid IL or missing references) //IL_00e0: Unknown result type (might be due to invalid IL or missing references) Player instance = Player.Instance; if (!((Object)(object)instance == (Object)null)) { int count; int count2; try { count = Earshot.CitizensWhoCanHear((Actor)(object)instance, shouted: false).Count; count2 = Earshot.CitizensWhoCanHear((Actor)(object)instance, shouted: true).Count; } catch { return; } if (count != 0 || count2 != 0) { Rect val = default(Rect); ((Rect)(ref val))..ctor(18f, (float)Screen.height - 74f, 260f, 56f); GUI.color = new Color(0f, 0f, 0f, 0.45f); GUI.Box(val, GUIContent.none); GUI.color = Color.white; GUILayout.BeginArea(new Rect(((Rect)(ref val)).x + 10f, ((Rect)(ref val)).y + 6f, ((Rect)(ref val)).width - 20f, ((Rect)(ref val)).height - 12f)); GUILayout.Label("Speaking reaches " + count, (Il2CppReferenceArray)null); GUILayout.Label("Shouting reaches " + count2, (Il2CppReferenceArray)null); GUILayout.EndArea(); } } } private static int CountListeners() { try { Player instance = Player.Instance; if ((Object)(object)instance == (Object)null) { return 0; } int num = 0; foreach (Citizen item in Earshot.CitizensWhoCanHear((Actor)(object)instance, _shouted)) { if (!((Object)(object)item == (Object)null) && (_targetId == 0 || ((Human)item).humanID != _targetId)) { num++; } } return num; } catch { return 0; } } private static void Submit() { string text = (_text ?? string.Empty).Trim(); if (text.Length == 0) { Plugin.Log.LogInfo((object)"Nothing was typed, so nothing was said. Keeping the box open and asking for the keyboard again."); _focusRequested = true; return; } Action onSubmit = _onSubmit; Close(); if (onSubmit == null) { return; } try { onSubmit(text); } catch (Exception ex) { Plugin.Log.LogError((object)("Submitting a line failed: " + ex)); } } private static string SafeName(Citizen c) { try { return ((Object)(object)c != (Object)null) ? ((Human)c).GetCasualName() : "them"; } catch { return "them"; } } } public static class ConversationOrchestrator { private static readonly Dictionary Busy = new Dictionary(); private const int ThinkingBeatAfterMs = 900; private static float StuckAfterSeconds => (float)ModConfig.RequestTimeoutSeconds.Value * 3f + 15f; public static bool IsBusy(Citizen citizen) { if ((Object)(object)citizen == (Object)null) { return false; } if (!Busy.TryGetValue(((Human)citizen).humanID, out var value)) { return false; } if (Time.realtimeSinceStartup - value < StuckAfterSeconds) { return true; } Busy.Remove(((Human)citizen).humanID); Plugin.Log.LogWarning((object)(((Human)citizen).GetCasualName() + " was still marked as thinking after " + (int)StuckAfterSeconds + "s. Clearing it - a reply that late is never coming, and leaving it would have hidden the option for the rest of the session.")); return false; } public static void Speak(Citizen citizen, string playerLine, bool shouted, string vanillaLine = null) { if ((Object)(object)citizen == (Object)null || string.IsNullOrWhiteSpace(playerLine)) { return; } int id = ((Human)citizen).humanID; if (IsBusy(citizen)) { Plugin.Log.LogInfo((object)("Ignoring a second line while " + ((Human)citizen).GetCasualName() + " is still thinking about the first.")); return; } Busy[id] = Time.realtimeSinceStartup; string systemPrompt; string turnMessage; IReadOnlyList history; try { CitizenSnapshot s = ContextBuilder.Build(citizen, shouted, vanillaLine); systemPrompt = PromptBuilder.BuildSystemPrompt(s); turnMessage = PromptBuilder.BuildTurnMessage(s, playerLine); history = ConversationMemory.Get(id); } catch (Exception ex) { Busy.Remove(id); Plugin.Log.LogError((object)("Could not build conversation context: " + ex)); return; } SpeechRelay.PlayerSaid(citizen, playerLine, shouted); int earshot = 0; try { earshot = Earshot.CitizensWhoCanHear((Actor)(object)citizen, shouted).Count; } catch { } bool settled = false; Task.Run(async delegate { NpcReply reply = null; try { Task generating = Player2Client.GenerateReplyAsync(systemPrompt, history, turnMessage, default(CancellationToken), retryIfUnusable: true); if (await Task.WhenAny(generating, Task.Delay(900)).ConfigureAwait(continueOnCapturedContext: false) != generating) { MainThread.Post(delegate { if (!settled) { SpeechRelay.ShowThinking(Humans.Resolve(id)); } }); } reply = await generating.ConfigureAwait(continueOnCapturedContext: false); } catch (Exception ex2) { Exception ex3 = ex2; Exception e = ex3; MainThread.Post(delegate { Plugin.Log.LogError((object)("Generation failed: " + e.Message)); }); } MainThread.Post(delegate { settled = true; try { Complete(id, playerLine, shouted, reply, earshot, systemPrompt, turnMessage); } finally { Busy.Remove(id); } }); }); } private static void Complete(int id, string playerLine, bool shouted, NpcReply reply, int earshot, string systemPrompt, string turnMessage) { Citizen val = Humans.Resolve(id); if ((Object)(object)val == (Object)null) { Plugin.Log.LogInfo((object)"The citizen this reply was meant for is no longer around; dropping it."); return; } string citizenName = ((Human)val).GetCitizenName(); if (reply == null || string.IsNullOrWhiteSpace(reply.Speech)) { SpeechRelay.ShowUnavailable(val); SessionLog.Exchange(citizenName, shouted, earshot, playerLine, reply?.LatencyMs ?? 0, reply?.Raw, null, 0f, 0f, null, null, null, systemPrompt, turnMessage); return; } SpeechRelay.CitizenSays(val, reply.Speech, shouted); ConversationMemory.Record(((Human)val).humanID, playerLine, reply.Speech); WorldEffectExecutor.EffectReport effectReport = null; try { effectReport = WorldEffectExecutor.Apply(val, reply, shouted, playerLine); } catch (Exception ex) { Plugin.Log.LogError((object)("Applying world effects failed: " + ex)); } SessionLog.Exchange(citizenName, shouted, earshot, playerLine, reply.LatencyMs, reply.Raw, reply.Speech, reply.Truthfulness, reply.Alarm, reply.Reason, effectReport?.Applied, effectReport?.Rejected, systemPrompt, turnMessage); if (!reply.WellFormed) { Plugin.Log.LogWarning((object)("The model ignored the reply schema for " + citizenName + ", so this turn could not carry any consequences.")); } if (ModConfig.VerboseLogging.Value) { string text = ((effectReport != null && effectReport.Applied.Count > 0) ? string.Join(", ", effectReport.Applied) : "none"); string text2 = ((effectReport != null && effectReport.Rejected.Count > 0) ? string.Join(", ", effectReport.Rejected) : "none"); Plugin.Log.LogInfo((object)(((Human)val).GetCasualName() + " replied in " + reply.LatencyMs + " ms (truthfulness " + reply.Truthfulness.ToString("0.00") + ", alarm " + reply.Alarm.ToString("0.00") + "). Applied: " + text + ". Refused: " + text2 + (string.IsNullOrWhiteSpace(reply.Reason) ? "" : (" | reasoning: " + reply.Reason)))); } try { BystanderReactions.Propagate(val, reply, shouted); } catch (Exception ex2) { Plugin.Log.LogWarning((object)("Bystander propagation failed: " + ex2.Message)); } if (ModConfig.EnableTts.Value) { string speech = reply.Speech; Player2Client.SpeakAsync(speech); } } } public abstract class CustomDialogPreset { public string Name { get; protected set; } public DialogPreset Preset { get; protected set; } public abstract bool IsAvailable(DialogPreset preset, Citizen saysTo, SideJob jobRef); public abstract void RunDialogMethod(DialogController instance, Citizen saysTo, Interactable saysToInteractable, NewNode where, Actor saidBy, bool success, NewRoom roomRef, SideJob jobRef); public virtual ForceSuccess ShouldDialogSucceedOverride(DialogController instance, DialogOption dialog, Citizen saysTo, NewNode where, Actor saidBy) { return (ForceSuccess)0; } protected static DialogPreset NewPreset(string name, string msgID, int ranking) { DialogPreset val = ScriptableObject.CreateInstance(); ((Object)val).hideFlags = (HideFlags)61; ((Object)val).name = name; val.msgID = msgID; val.defaultOption = true; val.tiedToKey = (DataKey)4; val.ranking = ranking; val.removeAfterSaying = false; val.useSuccessTest = false; val.baseChance = 1f; val.affectChanceIfRestrained = 0f; val.specialCase = (SpecialCase)0; return val; } } public static class DdsAuthoring { private const string BlockDictionary = "dds.blocks"; public static string CreateMessage(string text, string debugName) { //IL_0079: Unknown result type (might be due to invalid IL or missing references) //IL_007e: Unknown result type (might be due to invalid IL or missing references) //IL_0086: Unknown result type (might be due to invalid IL or missing references) //IL_0099: Expected O, but got Unknown //IL_00af: Unknown result type (might be due to invalid IL or missing references) //IL_00b4: Unknown result type (might be due to invalid IL or missing references) //IL_00bc: Unknown result type (might be due to invalid IL or missing references) //IL_00d0: Expected O, but got Unknown try { Toolbox instance = Toolbox.Instance; if ((Object)(object)instance == (Object)null || instance.allDDSBlocks == null || instance.allDDSMessages == null) { Plugin.Log.LogWarning((object)("DDS dictionaries are not loaded yet; cannot author '" + debugName + "'.")); return null; } string text2 = Guid.NewGuid().ToString(); string text3 = Guid.NewGuid().ToString(); DDSBlockSave val = new DDSBlockSave { id = text2, name = debugName + "_block" }; instance.allDDSBlocks[text2] = val; RegisterBlockText(text2, text); DDSMessageSave val2 = new DDSMessageSave { id = text3, name = debugName + "_msg" }; val2.AddBlock(text2); instance.allDDSMessages[text3] = val2; if (ModConfig.VerboseLogging.Value) { Plugin.Log.LogInfo((object)("Authored DDS message " + debugName + " -> " + text3)); } return text3; } catch (Exception ex) { Plugin.Log.LogError((object)("Authoring DDS message '" + debugName + "' failed: " + ex)); return null; } } private static void RegisterBlockText(string blockId, string text) { //IL_0001: Unknown result type (might be due to invalid IL or missing references) //IL_0006: Unknown result type (might be due to invalid IL or missing references) //IL_000e: Unknown result type (might be due to invalid IL or missing references) //IL_0017: Expected O, but got Unknown DisplayString entry = new DisplayString { displayStr = text, alternateStr = text }; Put(Strings.stringTable, blockId, entry); Dictionary> stringTableENG = Strings.stringTableENG; if (stringTableENG != null && stringTableENG != Strings.stringTable) { Put(stringTableENG, blockId, entry); } } private static void Put(Dictionary> table, string key, DisplayString entry) { if (table != null) { Dictionary val = default(Dictionary); if (!table.TryGetValue("dds.blocks", ref val) || val == null) { val = (table["dds.blocks"] = new Dictionary()); } val[key] = entry; } } } public static class DialogRegistry { [HarmonyPatch(typeof(DialogController), "Start")] public static class DialogController_Start { public static void Postfix(DialogController __instance) { try { Enumerator enumerator = __instance.dialogRef.GetEnumerator(); while (enumerator.MoveNext()) { KeyValuePair current = enumerator.Current; if ((SoCustomComparison)(object)current.Key != (SoCustomComparison)null && ((Object)current.Key).name == "WarnNotewriter") { _borrowedMethod = current.Value; break; } } if (_borrowedMethod == (MethodInfo)null) { Plugin.Log.LogError((object)"Could not find the WarnNotewriter entry in DialogController. Dialogue options will not respond. The game version may have changed."); return; } BuildPresets(); foreach (CustomDialogPreset value in Interceptors.Values) { WireInterceptor(value); } } catch (Exception ex) { Plugin.Log.LogError((object)("DialogController.Start patch failed: " + ex)); } } } [HarmonyPatch(typeof(DialogController), "WarnNotewriter")] public static class DialogController_WarnNotewriter { public static bool Prefix(DialogController __instance, Citizen saysTo, Interactable saysToInteractable, NewNode where, Actor saidBy, bool success, NewRoom roomRef, SideJob jobRef) { CustomDialogPreset customDialogPreset = Lookup(__instance.preset); if (customDialogPreset == null) { customDialogPreset = TakeChosen(); if (customDialogPreset != null) { ComplainOnce("DialogController.preset was not the option the player chose; fell back to what ExecuteDialog was called with this frame. " + customDialogPreset.Name + " still ran."); } } if (customDialogPreset == null) { return true; } Plugin.Log.LogInfo((object)(customDialogPreset.Name + " chosen" + (((Object)(object)saysTo != (Object)null) ? (" for " + ((Human)saysTo).GetCasualName()) : " with nobody on the other end"))); try { customDialogPreset.RunDialogMethod(__instance, saysTo, saysToInteractable, where, saidBy, success, roomRef, jobRef); } catch (Exception ex) { Plugin.Log.LogError((object)("Running " + customDialogPreset.Name + " failed: " + ex)); } return false; } } [HarmonyPatch(typeof(DialogController), "TestSpecialCaseAvailability")] public static class DialogController_TestSpecialCaseAvailability { public static bool Prefix(ref bool __result, DialogPreset preset, Citizen saysTo, SideJob jobRef) { CustomDialogPreset customDialogPreset = Lookup(preset); if (customDialogPreset == null) { return true; } try { __result = (Object)(object)saysTo != (Object)null && customDialogPreset.IsAvailable(preset, saysTo, jobRef); } catch (Exception ex) { Plugin.Log.LogWarning((object)("Availability check for " + customDialogPreset.Name + " failed: " + ex.Message)); __result = false; } return false; } } [HarmonyPatch(typeof(DialogController), "ExecuteDialog")] public static class DialogController_ExecuteDialog { public static void Prefix(DialogController __instance, DialogOption dialog, Interactable saysTo, NewNode where, Actor saidBy, ref ForceSuccess forceSuccess) { //IL_0076: Unknown result type (might be due to invalid IL or missing references) //IL_007c: Expected I4, but got Unknown if (dialog == null) { return; } CustomDialogPreset customDialogPreset = Lookup(dialog.preset); if (customDialogPreset == null) { return; } RememberChosen(customDialogPreset); if (forceSuccess) { return; } Citizen saysTo2 = null; try { if (saysTo != null && (Object)(object)saysTo.isActor != (Object)null) { saysTo2 = ((Il2CppObjectBase)saysTo.isActor).TryCast(); } } catch { } try { forceSuccess = (ForceSuccess)(int)customDialogPreset.ShouldDialogSucceedOverride(__instance, dialog, saysTo2, where, saidBy); } catch (Exception ex) { Plugin.Log.LogWarning((object)("Success override for " + customDialogPreset.Name + " failed: " + ex.Message)); } } } public static readonly Dictionary Interceptors = new Dictionary(); private static MethodInfo _borrowedMethod; private static bool _presetsBuilt; private static int _insertedAt; private static CustomDialogPreset _chosen; private static int _chosenFrame = -1; private static string _lastComplaint; public static void BuildPresets() { if (!_presetsBuilt) { string text = DdsAuthoring.CreateMessage("Say something...", "Player2_SpeakFreely"); string text2 = DdsAuthoring.CreateMessage("Shout something...", "Player2_Shout"); if (text != null && text2 != null) { Register(new SpeakFreelyPreset(text)); Register(new ShoutPreset(text2)); _presetsBuilt = true; Plugin.Log.LogInfo((object)"Dialogue options registered."); } } } private static void Register(CustomDialogPreset custom) { Interceptors[custom.Name] = custom; try { Toolbox instance = Toolbox.Instance; if ((Object)(object)instance != (Object)null) { if (instance.allDialog != null && !instance.allDialog.Contains(custom.Preset)) { instance.allDialog.Add(custom.Preset); } if (instance.defaultDialogOptions != null && !instance.defaultDialogOptions.Contains(custom.Preset)) { instance.defaultDialogOptions.Insert(_insertedAt++, custom.Preset); } } } catch (Exception ex) { Plugin.Log.LogError((object)("Could not add " + custom.Name + " to the dialog lists: " + ex)); } WireInterceptor(custom); } private static void WireInterceptor(CustomDialogPreset custom) { if (_borrowedMethod == (MethodInfo)null) { return; } try { DialogController instance = DialogController.Instance; if (!((Object)(object)instance == (Object)null) && instance.dialogRef != null && !instance.dialogRef.ContainsKey(custom.Preset)) { instance.dialogRef.Add(custom.Preset, _borrowedMethod); } } catch (Exception ex) { Plugin.Log.LogError((object)("Could not wire " + custom.Name + ": " + ex)); } } private static CustomDialogPreset Lookup(DialogPreset preset) { if ((SoCustomComparison)(object)preset == (SoCustomComparison)null) { return null; } try { IntPtr pointer = ((Il2CppObjectBase)preset).Pointer; foreach (CustomDialogPreset value2 in Interceptors.Values) { DialogPreset preset2 = value2.Preset; if ((SoCustomComparison)(object)preset2 != (SoCustomComparison)null && ((Il2CppObjectBase)preset2).Pointer == pointer) { return value2; } } } catch { } try { if (string.IsNullOrEmpty(((Object)preset).name)) { return null; } CustomDialogPreset value; return Interceptors.TryGetValue(((Object)preset).name, out value) ? value : null; } catch { return null; } } private static void RememberChosen(CustomDialogPreset custom) { _chosen = custom; _chosenFrame = Time.frameCount; } private static CustomDialogPreset TakeChosen() { CustomDialogPreset chosen = _chosen; if (chosen == null || Time.frameCount != _chosenFrame) { return null; } _chosen = null; _chosenFrame = -1; return chosen; } private static void ComplainOnce(string message) { if (!(_lastComplaint == message)) { _lastComplaint = message; Plugin.Log.LogWarning((object)message); } } } public static class GameInput { private static bool _held; public static bool Held => _held; public static void Claim() { _held = true; Apply(ours: true); } public static void Release() { if (_held) { _held = false; Apply(ours: false); } } public static void Tick() { if (_held) { Apply(ours: true); } } private static void Apply(bool ours) { try { InputController instance = InputController.Instance; if ((Object)(object)instance != (Object)null) { instance.SetMouseInputMode(ours, true); instance.SetCursorVisible(ours); instance.SetCursorLock(!ours); instance.enableInput = !ours; } } catch (Exception ex) { if (ModConfig.VerboseLogging.Value) { Plugin.Log.LogWarning((object)("Could not change the game's mouse mode: " + ex.Message)); } } try { Player instance2 = Player.Instance; if ((Object)(object)instance2 != (Object)null) { instance2.EnablePlayerMovement(!ours, true); instance2.EnablePlayerMouseLook(!ours, false); } } catch (Exception ex2) { if (ModConfig.VerboseLogging.Value) { Plugin.Log.LogWarning((object)("Could not suspend player movement: " + ex2.Message)); } } try { Cursor.lockState = (CursorLockMode)(!ours); Cursor.visible = ours; } catch { } } } public static class NpcConversation { private static float _nextAttempt; private static bool _busy; private static readonly Dictionary Cooldowns = new Dictionary(); public static string LastExchange { get; private set; } = "None yet."; public static void Tick() { if (!ModConfig.EnableNpcConversations.Value || _busy || Time.time < _nextAttempt) { return; } _nextAttempt = Time.time + ModConfig.NpcConversationInterval.Value; try { if (FindPair(out var a, out var b)) { Begin(a, b); } } catch (Exception ex) { if (ModConfig.VerboseLogging.Value) { Plugin.Log.LogWarning((object)("Looking for a conversation to start failed: " + ex.Message)); } } } private static bool FindPair(out Citizen a, out Citizen b) { //IL_00fc: Unknown result type (might be due to invalid IL or missing references) //IL_0108: Unknown result type (might be due to invalid IL or missing references) a = null; b = null; Player instance = Player.Instance; if ((Object)(object)instance == (Object)null) { return false; } List list = Earshot.CitizensWhoCanHear((Actor)(object)instance, shouted: false); if (list.Count < 2) { return false; } for (int i = 0; i < list.Count; i++) { for (int j = i + 1; j < list.Count; j++) { Citizen val = list[i]; Citizen val2 = list[j]; if ((Object)(object)val == (Object)null || (Object)(object)val2 == (Object)null || !Suitable(val) || !Suitable(val2)) { continue; } try { if ((Object)(object)((Actor)val).currentRoom == (Object)null || (Object)(object)((Actor)val2).currentRoom == (Object)null || ((Actor)val).currentRoom.roomID != ((Actor)val2).currentRoom.roomID || Vector3.Distance(((Component)val).transform.position, ((Component)val2).transform.position) > ModConfig.TalkRadius.Value) { continue; } goto IL_012d; } catch { } continue; IL_012d: if (OnCooldown(val, val2)) { continue; } a = val; b = val2; return true; } } return false; } private static bool Suitable(Citizen c) { try { if (((Actor)c).isDead || ((Actor)c).isAsleep || ((Actor)c).isStunned) { return false; } if ((Object)(object)((Actor)c).ai == (Object)null) { return false; } if (((Actor)c).ai.inCombat || ((Actor)c).ai.inFleeState || ((Actor)c).ai.restrained) { return false; } if (ConversationOrchestrator.IsBusy(c)) { return false; } return true; } catch { return false; } } private static long PairKey(Citizen a, Citizen b) { int num = Math.Min(((Human)a).humanID, ((Human)b).humanID); int num2 = Math.Max(((Human)a).humanID, ((Human)b).humanID); return ((long)num << 32) | (uint)num2; } private static bool OnCooldown(Citizen a, Citizen b) { long key = PairKey(a, b); if (Cooldowns.TryGetValue(key, out var value) && Time.time < value) { return true; } return false; } private static void Begin(Citizen a, Citizen b) { if (!RequestBudget.TryTake(RequestBudget.Kind.Overheard, a)) { return; } _busy = true; Cooldowns[PairKey(a, b)] = Time.time + ModConfig.NpcConversationCooldown.Value; string prompt; try { prompt = BuildPrompt(a, b); } catch (Exception ex) { _busy = false; RequestBudget.Finished(RequestBudget.Kind.Overheard); Plugin.Log.LogWarning((object)("Could not build a conversation prompt: " + ex.Message)); return; } Task.Run(async delegate { NpcReply raw = null; try { raw = await Player2Client.GenerateReplyAsync(prompt, null, "Write the exchange now.").ConfigureAwait(continueOnCapturedContext: false); } catch (Exception ex2) { Exception ex3 = ex2; Exception e = ex3; MainThread.Post(delegate { Plugin.Log.LogWarning((object)("Conversation generation failed: " + e.Message)); }); } NpcReply captured = raw; MainThread.Post(delegate { try { Play(a, b, captured); } finally { _busy = false; RequestBudget.Finished(RequestBudget.Kind.Overheard); } }); }); } private static string BuildPrompt(Citizen a, Citizen b) { StringBuilder stringBuilder = new StringBuilder(); CitizenSnapshot citizenSnapshot = ContextBuilder.Build(a, shouted: false, null); CitizenSnapshot citizenSnapshot2 = ContextBuilder.Build(b, shouted: false, null); stringBuilder.AppendLine("Write a short overheard exchange between two citizens of a rain-soaked voxel noir city."); stringBuilder.AppendLine("They are talking to each other, not to the player. Nobody is being interviewed."); stringBuilder.AppendLine(); stringBuilder.AppendLine("# " + citizenSnapshot.FullName); if (!string.IsNullOrEmpty(citizenSnapshot.Job)) { stringBuilder.AppendLine("Work: " + citizenSnapshot.Job); } if (citizenSnapshot.Traits.Count > 0) { stringBuilder.AppendLine("Traits: " + string.Join(", ", citizenSnapshot.Traits)); } if (citizenSnapshot.GroundTruth.Count > 0) { stringBuilder.AppendLine("Knows: " + string.Join(" ", citizenSnapshot.GroundTruth)); } stringBuilder.AppendLine(); stringBuilder.AppendLine("# " + citizenSnapshot2.FullName); if (!string.IsNullOrEmpty(citizenSnapshot2.Job)) { stringBuilder.AppendLine("Work: " + citizenSnapshot2.Job); } if (citizenSnapshot2.Traits.Count > 0) { stringBuilder.AppendLine("Traits: " + string.Join(", ", citizenSnapshot2.Traits)); } if (citizenSnapshot2.GroundTruth.Count > 0) { stringBuilder.AppendLine("Knows: " + string.Join(" ", citizenSnapshot2.GroundTruth)); } stringBuilder.AppendLine(); stringBuilder.AppendLine("# Where they are"); if (!string.IsNullOrEmpty(citizenSnapshot.LocationName)) { stringBuilder.AppendLine(citizenSnapshot.LocationName + ", " + citizenSnapshot.TimeOfDay); } stringBuilder.AppendLine("A private investigator is standing close enough to overhear them, which they have not noticed. Do not have them address that person."); stringBuilder.AppendLine(); stringBuilder.AppendLine("# How to answer"); stringBuilder.AppendLine("Reply with a single JSON object and nothing else:"); stringBuilder.AppendLine("{"); stringBuilder.AppendLine(" \"lines\": [ { \"who\": \"first or second\", \"says\": \"one short line\" } ],"); stringBuilder.AppendLine(" \"gossip\": { \"teller\": \"first or second\", \"about\": \"full name of a person mentioned\" }"); stringBuilder.AppendLine("}"); stringBuilder.AppendLine(); stringBuilder.AppendLine("Between two and " + ModConfig.NpcConversationLines.Value + " lines, alternating."); stringBuilder.AppendLine("Clipped, period-appropriate, ordinary. Small talk, complaints, rumours."); stringBuilder.AppendLine("Include gossip only if one of them genuinely mentions having seen a named person."); stringBuilder.AppendLine("Only these names may be mentioned as having been seen:"); List list = Testimony.PossibleSubjects(a, 4); List list2 = Testimony.PossibleSubjects(b, 4); if (list.Count == 0 && list2.Count == 0) { stringBuilder.AppendLine(" nobody - leave gossip out entirely."); } else { foreach (string item in list) { stringBuilder.AppendLine(" " + item + " (seen by first)"); } foreach (string item2 in list2) { stringBuilder.AppendLine(" " + item2 + " (seen by second)"); } } return stringBuilder.ToString(); } private static void Play(Citizen a, Citizen b, NpcReply raw) { if (raw == null || string.IsNullOrWhiteSpace(raw.Raw)) { return; } NpcExchange npcExchange = null; try { npcExchange = Player2Client.ParseExchange(raw.Raw); } catch (Exception ex) { Plugin.Log.LogWarning((object)("Could not read the exchange: " + ex.Message)); } if (npcExchange == null || npcExchange.Lines == null || npcExchange.Lines.Count == 0) { SessionLog.Note("Overheard exchange discarded: nothing usable came back."); return; } StringBuilder stringBuilder = new StringBuilder(); int num = Mathf.Min(npcExchange.Lines.Count, ModConfig.NpcConversationLines.Value); for (int i = 0; i < num; i++) { ExchangeLine exchangeLine = npcExchange.Lines[i]; if (exchangeLine != null && !string.IsNullOrWhiteSpace(exchangeLine.Says)) { Citizen val = (IsFirst(exchangeLine.Who) ? a : b); Citizen val2 = (IsFirst(exchangeLine.Who) ? b : a); float seconds = (float)i * ModConfig.NpcConversationLineGap.Value; string text = exchangeLine.Says; Citizen s = val; Citizen l = val2; DelayedSpeech.Queue(seconds, delegate { SpeechRelay.CitizenSaysTo(s, l, text); }); stringBuilder.AppendLine(" " + ((Human)val).GetCitizenName() + ": " + text); } } string text2 = ApplyGossip(a, b, npcExchange.Gossip); LastExchange = ((Human)a).GetCitizenName() + " and " + ((Human)b).GetCitizenName() + ((text2 != null) ? (", who passed on " + text2) : ""); SessionLog.Note("Overheard - " + ((Human)a).GetCitizenName() + " and " + ((Human)b).GetCitizenName() + Environment.NewLine + stringBuilder.ToString().TrimEnd() + ((text2 != null) ? (Environment.NewLine + " gossip: " + text2) : "")); } private static bool IsFirst(string who) { return string.IsNullOrEmpty(who) || who.Trim().ToLowerInvariant().StartsWith("f"); } private static string ApplyGossip(Citizen a, Citizen b, GossipItem gossip) { if (!ModConfig.NpcGossipSpreads.Value) { return null; } if (gossip == null || string.IsNullOrWhiteSpace(gossip.About)) { return null; } try { Citizen val = (IsFirst(gossip.Teller) ? a : b); Citizen val2 = (IsFirst(gossip.Teller) ? b : a); if (((Human)val).lastSightings == null) { return null; } Enumerator enumerator = ((Human)val).lastSightings.GetEnumerator(); while (enumerator.MoveNext()) { KeyValuePair current = enumerator.Current; Human key = current.Key; if (!((Object)(object)key == (Object)null)) { string citizenName = key.GetCitizenName(); if (!string.IsNullOrEmpty(citizenName) && citizenName.IndexOf(gossip.About.Trim(), StringComparison.OrdinalIgnoreCase) >= 0) { ((Human)val2).UpdateLastSighting(key, false, 0); return citizenName + ", from " + ((Human)val).GetCitizenName() + " to " + ((Human)val2).GetCitizenName(); } } } } catch (Exception ex) { if (ModConfig.VerboseLogging.Value) { Plugin.Log.LogWarning((object)("Passing gossip on failed: " + ex.Message)); } } return null; } } public static class DelayedSpeech { private static readonly List> Pending = new List>(); public static void Queue(float seconds, Action action) { if (action != null) { Pending.Add(new KeyValuePair(Time.time + seconds, action)); } } public static void Tick() { if (Pending.Count == 0) { return; } for (int num = Pending.Count - 1; num >= 0; num--) { if (!(Time.time < Pending[num].Key)) { Action value = Pending[num].Value; Pending.RemoveAt(num); try { value(); } catch (Exception ex) { Plugin.Log.LogWarning((object)("A delayed line threw: " + ex.Message)); } } } } public static void Clear() { Pending.Clear(); } } public static class SettingsWindow { private const int WindowId = 1345476402; private static bool _open; private static Rect _rect = new Rect(120f, 90f, 560f, 640f); private static Vector2 _scroll; private static int _tab; private static readonly string[] Tabs = new string[5] { "Connection", "Talking", "Consequences", "Appearance", "Debug" }; private static string _probeResult = ""; private static string _goalDump = ""; private static readonly string[] Themes = new string[5] { "Rain", "Neon", "Amber", "Paper", "Game default" }; private static bool _probing; public static bool IsOpen => _open; public static void Toggle() { if (_open) { Close(); } else { Open(); } } public static void Open() { if (!_open) { _open = true; ((Rect)(ref _rect)).x = Mathf.Clamp(((Rect)(ref _rect)).x, 0f, Mathf.Max(0f, (float)Screen.width - 200f)); ((Rect)(ref _rect)).y = Mathf.Clamp(((Rect)(ref _rect)).y, 0f, Mathf.Max(0f, (float)Screen.height - 120f)); GameInput.Claim(); } } public static void Close() { if (_open) { _open = false; GameInput.Release(); } } public static void Draw() { //IL_0037: 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_003d: Unknown result type (might be due to invalid IL or missing references) //IL_0042: Unknown result type (might be due to invalid IL or missing references) //IL_004e: Unknown result type (might be due to invalid IL or missing references) //IL_0053: 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_0093: Unknown result type (might be due to invalid IL or missing references) //IL_0098: Unknown result type (might be due to invalid IL or missing references) //IL_00a5: Unknown result type (might be due to invalid IL or missing references) if (_open) { GUI.depth = -1000; float num = Mathf.Clamp(ModConfig.UiScale.Value, 0.6f, 2.5f); Matrix4x4 matrix = GUI.matrix; GUI.matrix = Matrix4x4.TRS(Vector3.zero, Quaternion.identity, new Vector3(num, num, 1f)); Skin.Scope scope = Skin.Begin(); _rect = GUI.Window(1345476402, _rect, WindowFunction.op_Implicit((Action)DrawContents), "Loose Lips"); scope.End(); GUI.matrix = matrix; } } private static void DrawContents(int id) { //IL_001d: Unknown result type (might be due to invalid IL or missing references) //IL_0023: Unknown result type (might be due to invalid IL or missing references) //IL_0028: Unknown result type (might be due to invalid IL or missing references) //IL_0097: Unknown result type (might be due to invalid IL or missing references) //IL_009c: Unknown result type (might be due to invalid IL or missing references) //IL_0102: Unknown result type (might be due to invalid IL or missing references) GUILayout.Space(4f); DrawTabs(); GUILayout.Space(6f); _scroll = GUILayout.BeginScrollView(_scroll, (Il2CppReferenceArray)null); switch (_tab) { case 0: DrawConnection(); break; case 1: DrawTalking(); break; case 2: DrawConsequences(); break; case 3: DrawAppearance(); break; default: DrawDebug(); break; } GUILayout.EndScrollView(); GUILayout.Space(4f); GUILayout.BeginHorizontal((Il2CppReferenceArray)null); GUILayout.Label("Press " + ((object)ModConfig.SettingsHotkey.Value/*cast due to .constrained prefix*/).ToString() + " to close.", (Il2CppReferenceArray)null); GUILayout.FlexibleSpace(); if (GUILayout.Button("Close", (GUILayoutOption[])(object)new GUILayoutOption[1] { GUILayout.Width(90f) })) { Close(); } GUILayout.EndHorizontal(); GUI.DragWindow(new Rect(0f, 0f, 100000f, 22f)); } private static void DrawTabs() { //IL_0019: Unknown result type (might be due to invalid IL or missing references) //IL_001e: Unknown result type (might be due to invalid IL or missing references) //IL_0024: Unknown result type (might be due to invalid IL or missing references) //IL_0030: 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_0048: Unknown result type (might be due to invalid IL or missing references) //IL_004e: Unknown result type (might be due to invalid IL or missing references) //IL_008d: Unknown result type (might be due to invalid IL or missing references) GUILayout.BeginHorizontal((Il2CppReferenceArray)null); for (int i = 0; i < Tabs.Length; i++) { bool flag = i == _tab; Color backgroundColor = GUI.backgroundColor; if (flag) { GUI.backgroundColor = new Color(backgroundColor.r * 1.8f, backgroundColor.g * 1.8f, backgroundColor.b * 1.8f, backgroundColor.a); } if (GUILayout.Button(flag ? ("[ " + Tabs[i] + " ]") : Tabs[i], (Il2CppReferenceArray)null)) { _tab = i; } GUI.backgroundColor = backgroundColor; } GUILayout.EndHorizontal(); } private static void DrawConnection() { Header("Player2 app"); GUILayout.BeginHorizontal((Il2CppReferenceArray)null); GUILayout.Label(Player2Client.Available ? "Connected" : "Not detected", (GUILayoutOption[])(object)new GUILayoutOption[1] { GUILayout.Width(180f) }); if (GUILayout.Button(_probing ? "Testing..." : "Test connection", (GUILayoutOption[])(object)new GUILayoutOption[1] { GUILayout.Width(150f) }) && !_probing) { _probing = true; _probeResult = ""; Task task = Player2Client.ProbeAsync(); task.ContinueWith(delegate(Task t) { MainThread.Post(delegate { _probing = false; _probeResult = ((t.Status == TaskStatus.RanToCompletion && t.Result) ? "Reached the Player2 app." : ("No answer. Is the app running? " + Player2Client.LastError)); }); }); } GUILayout.EndHorizontal(); if (!string.IsNullOrEmpty(_probeResult)) { GUILayout.Label(_probeResult, (Il2CppReferenceArray)null); } GUILayout.Label(Player2Status.Describe(), (Il2CppReferenceArray)null); GUILayout.Space(6f); TextField(ModConfig.BaseUrl, "Base URL"); TextField(ModConfig.HealthPath, "Health path"); TextField(ModConfig.ChatPath, "Chat path"); TextField(ModConfig.TtsPath, "Text to speech path"); GUILayout.Label("If a path is wrong, open http://localhost:4315/docs and correct it here.", (Il2CppReferenceArray)null); GUILayout.Space(8f); Header("Model"); TextField(ModConfig.Model, "Model name (blank for the default)"); IntSlider(ModConfig.RequestTimeoutSeconds, "Give up after", 5, 120, " s"); Toggle(ModConfig.EnableTts, "Speak replies aloud through Player2"); } private static void DrawTalking() { Header("Conversation"); IntSlider(ModConfig.HistoryTurnsPerCitizen, "Remembered turns per person", 0, 64, ""); IntSlider(ModConfig.MaxReplyCharacters, "Longest reply", 60, 600, " characters"); Toggle(ModConfig.UseVanillaLinesAsInfluence, "Use the game's own lines as tone guidance"); Toggle(ModConfig.RememberBetweenSessions, "People remember you between sessions"); GUILayout.Label("The scripted answer is shown to the model as the register to write in, rather than being spoken word for word.", (Il2CppReferenceArray)null); GUILayout.Space(10f); Header("Voice reach"); FloatSlider(ModConfig.WhisperRadius, "Whispering carries", 0.5f, 8f, " m"); FloatSlider(ModConfig.TalkRadius, "Talking carries", 1f, 30f, " m"); FloatSlider(ModConfig.ShoutRadius, "Shouting carries", 5f, 90f, " m"); Toggle(ModConfig.ShowVoiceReachMeter, "Show the reach meter on screen"); Player instance = Player.Instance; if ((Object)(object)instance != (Object)null) { try { int count = Earshot.CitizensWhoCanHear((Actor)(object)instance, shouted: false).Count; int count2 = Earshot.CitizensWhoCanHear((Actor)(object)instance, shouted: true).Count; GUILayout.Space(4f); GUILayout.Label("Right now: speaking reaches " + count + ", shouting reaches " + count2 + ".", (Il2CppReferenceArray)null); } catch { } } GUILayout.Space(10f); Header("People reacting to what happens"); Toggle(ModConfig.EnableAmbientLife, "React to crimes, fights, fright and people bolting"); if (ModConfig.EnableAmbientLife.Value) { Toggle(ModConfig.ReactToWhatYouDo, "React to what you do - drawing a weapon, putting it away"); Toggle(ModConfig.GreetYouFirst, "People who know you speak first when you walk up"); if (ModConfig.GreetYouFirst.Value) { FloatSlider(ModConfig.GreetingDistance, "Close enough to greet you", 1f, 15f, " m"); } IntSlider(ModConfig.MaxAmbientPerHour, "At most", 0, 400, " per hour"); FloatSlider(ModConfig.MinSecondsBetweenAmbient, "No sooner than every", 5f, 300f, " s"); FloatSlider(ModConfig.PerCitizenCooldown, "Same person again after", 15f, 900f, " s"); IntSlider(ModConfig.MinJoulesForAmbient, "Keep in reserve", 0, 5000, " credits"); GUILayout.Label(RequestBudget.Summary(), (Il2CppReferenceArray)null); GUILayout.Label("Last: " + AmbientReactions.LastLine, (Il2CppReferenceArray)null); } GUILayout.Label("Each reaction is a few seconds of your own machine. One is generated at a time, never while you are waiting on a reply of your own.", (Il2CppReferenceArray)null); GUILayout.Space(10f); Header("Citizens talking to each other"); Toggle(ModConfig.EnableNpcConversations, "Let people near you strike up their own conversations"); if (ModConfig.EnableNpcConversations.Value) { Toggle(ModConfig.NpcGossipSpreads, "What they tell each other is genuinely learned"); FloatSlider(ModConfig.NpcConversationInterval, "Try one every", 20f, 600f, " s"); IntSlider(ModConfig.NpcConversationLines, "Longest exchange", 2, 8, " lines"); FloatSlider(ModConfig.NpcConversationLineGap, "Gap between lines", 1f, 10f, " s"); GUILayout.Label("Last one: " + NpcConversation.LastExchange, (Il2CppReferenceArray)null); } GUILayout.Label("They only talk where you can hear them. A conversation you cannot overhear has nothing in it for you and still costs a request.", (Il2CppReferenceArray)null); } private static void DrawConsequences() { Toggle(ModConfig.EnableWorldEffects, "Let conversations change the world"); if (!ModConfig.EnableWorldEffects.Value) { GUILayout.Label("Off: people will talk, but nothing they say has any effect.", (Il2CppReferenceArray)null); return; } GUILayout.Space(8f); Header("What a convincing line can do"); Toggle(ModConfig.AllowItemHandover, "Hand over the item they are holding"); Toggle(ModConfig.AllowPoliceRedirection, "Call police onto you, off you, or onto someone else"); Toggle(ModConfig.AllowCombatEffects, "Flee, fight, or surrender"); Toggle(ModConfig.AllowMoneyHandover, "Hand over cash they are carrying"); IntSlider(ModConfig.MaxMoneyPerLine, "Most one conversation can get", 0, 5000, ""); Toggle(ModConfig.AllowFollowing, "Agree to come along with you"); if (ModConfig.AllowFollowing.Value) { List list = FollowDirector.Names(); GUILayout.Label((list.Count == 0) ? "Nobody is with you." : ("With you: " + string.Join(", ", list)), (Il2CppReferenceArray)null); } Toggle(ModConfig.AllowAllegiance, "Take your side, or turn against you"); if (ModConfig.AllowAllegiance.Value) { Toggle(ModConfig.AlliesDefendYou, "Allies step in when you are attacked"); FloatSlider(ModConfig.AllyLikeThreshold, "Liking needed to side with you", 0f, 1f, ""); } Toggle(ModConfig.AllowThirdPartyOpinion, "Turn people against each other, or stand up for somebody"); if (ModConfig.AllowThirdPartyOpinion.Value) { FloatSlider(ModConfig.MaxOpinionShiftPerLine, "Most one line can change an opinion", 0f, 1f, ""); FloatSlider(ModConfig.LoyaltyResistance, "Closeness resists persuasion", 0f, 1f, ""); } Toggle(ModConfig.AllowNegotiation, "Name a price, and be paid it"); if (ModConfig.AllowNegotiation.Value) { IntSlider(ModConfig.MaxDemand, "Most anyone will ask", 0, 10000, ""); } Toggle(ModConfig.AllowTestimony, "Give up where and when they saw somebody"); GUILayout.Label("Uses the game's own witness mechanism, so what they give you is a real lead in the case file - and they can only name people they actually saw.", (Il2CppReferenceArray)null); Toggle(ModConfig.AllowGoalRedirection, "Change what they are doing - send them home, or over to look"); Toggle(ModConfig.AllowCrowdEffects, "Move everyone in earshot, not just the person you spoke to"); GUILayout.Space(10f); Header("Limits"); FloatSlider(ModConfig.MaxLikeShiftPerLine, "Most one line can move a relationship", 0f, 1f, ""); FloatSlider(ModConfig.MaxSuspicionShiftPerLine, "Most one line can move suspicion", 0f, 1f, ""); GUILayout.Label("Lower values mean it takes a real conversation to win somebody over, rather than a single lucky sentence.", (Il2CppReferenceArray)null); } private static void DrawAppearance() { //IL_0145: Unknown result type (might be due to invalid IL or missing references) //IL_014a: Unknown result type (might be due to invalid IL or missing references) Header("Theme"); GUILayout.BeginHorizontal((Il2CppReferenceArray)null); string[] themes = Themes; foreach (string text in themes) { if (GUILayout.Button((ModConfig.Theme.Value == text) ? ("[ " + text + " ]") : text, (Il2CppReferenceArray)null)) { ModConfig.Theme.Value = text; } } GUILayout.EndHorizontal(); GUILayout.Label("Rain is the game's own wet blue. Neon matches the mod's icon. Paper is a light theme for bright rooms. Game default leaves the colours alone.", (Il2CppReferenceArray)null); GUILayout.Space(8f); FloatSlider(ModConfig.AccentHue, "Shift the colour", -0.5f, 0.5f, ""); Toggle(ModConfig.TintTheChatBox, "Tint the typing box too"); GUILayout.Space(10f); Header("Size and transparency"); FloatSlider(ModConfig.UiScale, "Interface scale", 0.6f, 2.5f, "x"); GUILayout.Label("Raise this on a high resolution screen.", (Il2CppReferenceArray)null); FloatSlider(ModConfig.WindowOpacity, "Window opacity", 0.2f, 1f, ""); GUILayout.Label("Only the window itself fades - the text stays readable at any setting.", (Il2CppReferenceArray)null); GUILayout.Space(10f); Header("Hotkey"); GUILayout.Label("This window opens with " + ((object)ModConfig.SettingsHotkey.Value/*cast due to .constrained prefix*/).ToString() + ". Change it in the config file.", (Il2CppReferenceArray)null); } private static void DrawDebug() { Toggle(ModConfig.VerboseLogging, "Verbose logging"); GUILayout.Label("Writes the reply, how truthful it was, and which effects were applied or discarded into the BepInEx log.", (Il2CppReferenceArray)null); GUILayout.Space(8f); Toggle(ModConfig.LogPrompts, "Log every prompt"); GUILayout.Label("Large. Useful when a citizen answers oddly and you want to see what they were told.", (Il2CppReferenceArray)null); GUILayout.Space(10f); Header("Does any of this actually work?"); GUI.enabled = !CoreSelfTest.Running; if (GUILayout.Button(CoreSelfTest.Running ? "Testing..." : "Test the whole chain on the nearest person", (GUILayoutOption[])(object)new GUILayoutOption[1] { GUILayout.Width(320f) })) { CoreSelfTest.Run(); } GUI.enabled = true; GUILayout.Label(CoreSelfTest.LastSummary, (Il2CppReferenceArray)null); GUILayout.Label("Stand next to somebody, then run this. It walks every step - reading what they know, building the prompt, reaching the model, speaking the line, applying the consequences - and names the first one that fails.", (Il2CppReferenceArray)null); GUILayout.Space(10f); Header("Goal presets"); if (GUILayout.Button("Write the game's goal list to the transcript", (GUILayoutOption[])(object)new GUILayoutOption[1] { GUILayout.Width(320f) })) { _goalDump = GoalDirector.DumpPresetNames(); } if (!string.IsNullOrEmpty(_goalDump)) { GUILayout.Label(_goalDump, (Il2CppReferenceArray)null); } GUILayout.Label("Sending somebody home matches a goal preset by name, and those names live in the game's assets rather than its code. Run this once in a loaded save to see the real list and confirm the matches are right.", (Il2CppReferenceArray)null); GUILayout.Space(10f); Header("Transcript"); Toggle(ModConfig.WriteTranscript, "Keep a transcript of every exchange"); Toggle(ModConfig.TranscribePrompts, "Include the full prompts as well"); if (!string.IsNullOrEmpty(SessionLog.Path)) { GUILayout.Label(SessionLog.Path, (Il2CppReferenceArray)null); } GUILayout.Space(10f); Header("Session"); if (GUILayout.Button("Forget every conversation", (GUILayoutOption[])(object)new GUILayoutOption[1] { GUILayout.Width(240f) })) { ConversationMemory.Clear(); VanillaLineCapture.Clear(); Plugin.Log.LogInfo((object)"Conversation memory cleared from the settings window."); } GUILayout.Label("People will no longer remember anything you have said to them.", (Il2CppReferenceArray)null); } private static void Header(string text) { GUILayout.Label("— " + text + " —", (Il2CppReferenceArray)null); } private static void Toggle(ConfigEntry entry, string label) { bool flag = GUILayout.Toggle(entry.Value, " " + label, (Il2CppReferenceArray)null); if (flag != entry.Value) { entry.Value = flag; } } private static void TextField(ConfigEntry entry, string label) { GUILayout.Label(label, (Il2CppReferenceArray)null); string text = GUILayout.TextField(entry.Value ?? string.Empty, 200, Array.Empty()); if (text != entry.Value) { entry.Value = text; } } private static void FloatSlider(ConfigEntry entry, string label, float min, float max, string suffix) { GUILayout.Label(label + ": " + entry.Value.ToString("0.00") + suffix, (Il2CppReferenceArray)null); float num = GUILayout.HorizontalSlider(entry.Value, min, max, Array.Empty()); if (Mathf.Abs(num - entry.Value) > 0.0001f) { entry.Value = num; } } private static void IntSlider(ConfigEntry entry, string label, int min, int max, string suffix) { GUILayout.Label(label + ": " + entry.Value + suffix, (Il2CppReferenceArray)null); int num = Mathf.RoundToInt(GUILayout.HorizontalSlider((float)entry.Value, (float)min, (float)max, Array.Empty())); if (num != entry.Value) { entry.Value = num; } } } public static class Skin { public struct Scope { public Color Background; public Color Content; public bool Applied; public void End() { //IL_0011: Unknown result type (might be due to invalid IL or missing references) //IL_001d: Unknown result type (might be due to invalid IL or missing references) if (Applied) { GUI.backgroundColor = Background; GUI.contentColor = Content; } } } public static Scope Begin() { //IL_000b: Unknown result type (might be due to invalid IL or missing references) //IL_0010: Unknown result type (might be due to invalid IL or missing references) //IL_0017: Unknown result type (might be due to invalid IL or missing references) //IL_001c: Unknown result type (might be due to invalid IL or missing references) //IL_007a: Unknown result type (might be due to invalid IL or missing references) //IL_007f: Unknown result type (might be due to invalid IL or missing references) //IL_0084: Unknown result type (might be due to invalid IL or missing references) //IL_0085: Unknown result type (might be due to invalid IL or missing references) //IL_008b: 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) //IL_009c: Unknown result type (might be due to invalid IL or missing references) //IL_00a8: Unknown result type (might be due to invalid IL or missing references) //IL_0048: Unknown result type (might be due to invalid IL or missing references) //IL_004d: Unknown result type (might be due to invalid IL or missing references) //IL_004f: Unknown result type (might be due to invalid IL or missing references) //IL_0056: Unknown result type (might be due to invalid IL or missing references) //IL_005d: 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) Scope result = new Scope { Background = GUI.backgroundColor, Content = GUI.contentColor, Applied = true }; string value = ModConfig.Theme.Value; if (value == "Game default") { Color backgroundColor = GUI.backgroundColor; GUI.backgroundColor = new Color(backgroundColor.r, backgroundColor.g, backgroundColor.b, Opacity()); return result; } Color val = Shift(BaseColour(value)); GUI.backgroundColor = new Color(val.r, val.g, val.b, Opacity()); GUI.contentColor = ContentColour(value); return result; } private static float Opacity() { return Mathf.Clamp(ModConfig.WindowOpacity.Value, 0.2f, 1f); } private static Color BaseColour(string theme) { //IL_003d: Unknown result type (might be due to invalid IL or missing references) //IL_0042: Unknown result type (might be due to invalid IL or missing references) //IL_008a: Unknown result type (might be due to invalid IL or missing references) //IL_0054: Unknown result type (might be due to invalid IL or missing references) //IL_0059: 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_0070: Unknown result type (might be due to invalid IL or missing references) //IL_0082: Unknown result type (might be due to invalid IL or missing references) //IL_0087: Unknown result type (might be due to invalid IL or missing references) return (Color)(theme switch { "Neon" => new Color(0.3f, 0.82f, 0.88f), "Amber" => new Color(0.95f, 0.68f, 0.3f), "Paper" => new Color(0.92f, 0.9f, 0.86f), _ => new Color(0.42f, 0.55f, 0.72f), }); } private static Color ContentColour(string theme) { //IL_0023: Unknown result type (might be due to invalid IL or missing references) //IL_000d: Unknown result type (might be due to invalid IL or missing references) return (Color)((theme == "Paper") ? new Color(0.1f, 0.1f, 0.12f) : Color.white); } private static Color Shift(Color c) { //IL_0024: Unknown result type (might be due to invalid IL or missing references) //IL_0042: Unknown result type (might be due to invalid IL or missing references) //IL_0047: Unknown result type (might be due to invalid IL or missing references) //IL_001f: Unknown result type (might be due to invalid IL or missing references) //IL_0020: Unknown result type (might be due to invalid IL or missing references) //IL_004b: Unknown result type (might be due to invalid IL or missing references) float value = ModConfig.AccentHue.Value; if (Mathf.Abs(value) < 0.001f) { return c; } float num2 = default(float); float num3 = default(float); float num = default(float); Color.RGBToHSV(c, ref num, ref num2, ref num3); num = Mathf.Repeat(num + value, 1f); return Color.HSVToRGB(num, num2, num3); } } public sealed class SpeakFreelyPreset : CustomDialogPreset { public const string PresetName = "Player2_SpeakFreely"; public SpeakFreelyPreset(string msgID) { base.Name = "Player2_SpeakFreely"; base.Preset = CustomDialogPreset.NewPreset("Player2_SpeakFreely", msgID, 1); } public override bool IsAvailable(DialogPreset preset, Citizen saysTo, SideJob jobRef) { if ((Object)(object)saysTo == (Object)null) { return false; } if (((Actor)saysTo).isDead || ((Actor)saysTo).isAsleep || ((Actor)saysTo).isStunned) { return false; } if (ConversationOrchestrator.IsBusy(saysTo)) { return false; } return true; } public override void RunDialogMethod(DialogController instance, Citizen saysTo, Interactable saysToInteractable, NewNode where, Actor saidBy, bool success, NewRoom roomRef, SideJob jobRef) { if ((Object)(object)saysTo == (Object)null) { return; } int id = ((Human)saysTo).humanID; ChatOverlay.Open(saysTo, shouted: false, delegate(string line) { Citizen val = Humans.Resolve(id); if (!((Object)(object)val == (Object)null)) { ConversationOrchestrator.Speak(val, line, shouted: false, VanillaLineCapture.TakeLastFor(val)); } }); } } public sealed class ShoutPreset : CustomDialogPreset { public const string PresetName = "Player2_Shout"; public ShoutPreset(string msgID) { base.Name = "Player2_Shout"; base.Preset = CustomDialogPreset.NewPreset("Player2_Shout", msgID, 2); } public override bool IsAvailable(DialogPreset preset, Citizen saysTo, SideJob jobRef) { if ((Object)(object)saysTo == (Object)null) { return false; } if (((Actor)saysTo).isDead) { return false; } if (ConversationOrchestrator.IsBusy(saysTo)) { return false; } return true; } public override void RunDialogMethod(DialogController instance, Citizen saysTo, Interactable saysToInteractable, NewNode where, Actor saidBy, bool success, NewRoom roomRef, SideJob jobRef) { if ((Object)(object)saysTo == (Object)null) { return; } int id = ((Human)saysTo).humanID; ChatOverlay.Open(saysTo, shouted: true, delegate(string line) { Citizen val = Humans.Resolve(id); if (!((Object)(object)val == (Object)null)) { ConversationOrchestrator.Speak(val, line, shouted: true, VanillaLineCapture.TakeLastFor(val)); } }); } } public static class SpeechRelay { public static void PlayerSaid(Citizen listener, string line, bool shouted) { Player instance = Player.Instance; if (!((Object)(object)instance == (Object)null)) { Say((Actor)(object)instance, ((Object)(object)listener != (Object)null) ? ((Actor)listener).interactable : null, line, shouted, interupt: true); } } public static void CitizenSays(Citizen speaker, string line, bool shouted) { if (!((Object)(object)speaker == (Object)null)) { Player instance = Player.Instance; Say((Actor)(object)speaker, ((Object)(object)instance != (Object)null) ? ((Actor)instance).interactable : null, line, shouted, interupt: true); } } public static void CitizenSaysTo(Citizen speaker, Citizen listener, string line) { if (!((Object)(object)speaker == (Object)null)) { Say((Actor)(object)speaker, ((Object)(object)listener != (Object)null) ? ((Actor)listener).interactable : null, line, shouted: false, interupt: false); } } public static void CitizenSaysAt(Citizen speaker, string line, VoiceLevel level) { if (!((Object)(object)speaker == (Object)null)) { Say((Actor)(object)speaker, null, line, LooseLips.World.Voice.IsShout(level), interupt: false); } } public static void ShowThinking(Citizen speaker) { if (!((Object)(object)speaker == (Object)null)) { Say((Actor)(object)speaker, null, "...", shouted: false, interupt: false); } } public static void ShowUnavailable(Citizen speaker) { if (!((Object)(object)speaker == (Object)null)) { string line = (Player2Client_Available() ? "I heard you. I just have nothing to say to that." : "Sorry, I got nothing to say to you."); Plugin.Log.LogWarning((object)(((Human)speaker).GetCasualName() + " had nothing to say - " + (Player2Client_Available() ? "the model returned nothing usable, even after asking again." : "the Player2 app is not reachable."))); Say((Actor)(object)speaker, null, line, shouted: false, interupt: true); } } private static bool Player2Client_Available() { try { return Player2Client.Available; } catch { return false; } } private static void Say(Actor speaker, Interactable speakingTo, string line, bool shouted, bool interupt) { //IL_0079: Unknown result type (might be due to invalid IL or missing references) //IL_007f: Unknown result type (might be due to invalid IL or missing references) if ((Object)(object)speaker == (Object)null || string.IsNullOrWhiteSpace(line)) { return; } try { SpeechController speechController = speaker.speechController; if ((Object)(object)speechController == (Object)null) { Plugin.Log.LogWarning((object)("No speech controller on " + ((Object)speaker).name + "; line dropped.")); return; } string text = RuntimeStrings.Register(line); if (text != null) { speechController.Speak("player2.generated", text, false, shouted, interupt, 0f, false, default(Color), (Human)null, false, false, (SideJob)null, (DialogPreset)null, (AISpeechPreset)null, speakingTo, (InteractionDialogInstance)null); } } catch (Exception ex) { Plugin.Log.LogError((object)("Speaking a generated line failed: " + ex)); } } } public static class VanillaLineCapture { [HarmonyPatch(typeof(SpeechController), "Speak", new Type[] { typeof(string), typeof(bool), typeof(bool), typeof(Human), typeof(SideJob), typeof(InteractionDialogInstance) })] public static class SpeechController_Speak_Capture { public static void Prefix(SpeechController __instance, string ddsMessage) { if (!ModConfig.UseVanillaLinesAsInfluence.Value || string.IsNullOrEmpty(ddsMessage)) { return; } try { Actor actor = __instance.actor; Human val = (Human)(object)((actor is Human) ? actor : null); if ((Object)(object)val == (Object)null) { val = (((Object)(object)__instance.actor != (Object)null) ? ((Il2CppObjectBase)__instance.actor).TryCast() : null); } if ((Object)(object)val == (Object)null || ((Actor)val).isPlayer) { return; } Citizen val2 = ((Il2CppObjectBase)val).TryCast(); if ((Object)(object)val2 == (Object)null) { return; } List val4 = default(List); List val3 = val.ParseDDSMessage(ddsMessage, (Acquaintance)null, ref val4, false, (Object)null, false); if (val3 == null || val3.Count == 0) { return; } string text = string.Empty; Enumerator enumerator = val3.GetEnumerator(); while (enumerator.MoveNext()) { string current = enumerator.Current; if (!string.IsNullOrEmpty(current)) { if (text.Length > 0) { text += " "; } text += current; } } Remember(val2, text); } catch (Exception ex) { if (ModConfig.VerboseLogging.Value) { Plugin.Log.LogWarning((object)("Could not capture a vanilla line: " + ex.Message)); } } } } private static readonly Dictionary LastLine = new Dictionary(); public static void Remember(Citizen citizen, string line) { if (!((Object)(object)citizen == (Object)null) && !string.IsNullOrWhiteSpace(line)) { LastLine[((Human)citizen).humanID] = line; } } public static string TakeLastFor(Citizen citizen) { if ((Object)(object)citizen == (Object)null || !ModConfig.UseVanillaLinesAsInfluence.Value) { return null; } if (!LastLine.TryGetValue(((Human)citizen).humanID, out var value)) { return null; } LastLine.Remove(((Human)citizen).humanID); return value; } public static void Clear() { LastLine.Clear(); } } } namespace LooseLips.Core { public static class ConversationMemory { public sealed class StoredLine { public bool FromPlayer { get; set; } public string Text { get; set; } } private static readonly Dictionary> Threads = new Dictionary>(); private static string _loadedSeed; private static bool _dirty; private static float _nextFlush; public static IReadOnlyList Get(int citizenId) { List value; return Threads.TryGetValue(citizenId, out value) ? value : new List(); } public static bool HasHistory(int citizenId) { List value; return Threads.TryGetValue(citizenId, out value) && value.Count > 0; } public static int TurnsWith(int citizenId) { List value; return Threads.TryGetValue(citizenId, out value) ? (value.Count / 2) : 0; } public static void Record(int citizenId, string playerLine, string citizenLine) { if (!Threads.TryGetValue(citizenId, out var value)) { value = new List(); Threads[citizenId] = value; } if (!string.IsNullOrWhiteSpace(playerLine)) { value.Add(ChatMessage.User(playerLine)); } if (!string.IsNullOrWhiteSpace(citizenLine)) { value.Add(ChatMessage.Assistant(citizenLine)); } int num = ModConfig.HistoryTurnsPerCitizen.Value * 2; if (num <= 0) { value.Clear(); return; } while (value.Count > num) { value.RemoveAt(0); } _dirty = true; } public static void Flush() { if (_dirty && ModConfig.RememberBetweenSessions.Value && !(Time.time < _nextFlush)) { _nextFlush = Time.time + 20f; _dirty = false; Save(); } } public static void Forget(int citizenId) { Threads.Remove(citizenId); if (ModConfig.RememberBetweenSessions.Value) { Save(); } } public static void Clear() { Threads.Clear(); _loadedSeed = null; } private static string CurrentSeed() { try { CityData instance = CityData.Instance; if ((Object)(object)instance == (Object)null) { return null; } string seed = instance.seed; return string.IsNullOrWhiteSpace(seed) ? null : Sanitise(seed); } catch { return null; } } private static string PathForSeed(string seed) { try { string text = Path.Combine(Paths.BepInExRootPath, "LooseLips-memories"); Directory.CreateDirectory(text); return Path.Combine(text, seed + ".json"); } catch { return null; } } public static void EnsureLoaded() { if (!ModConfig.RememberBetweenSessions.Value) { return; } string text = CurrentSeed(); if (text == null || text == _loadedSeed) { return; } _loadedSeed = text; Threads.Clear(); string text2 = PathForSeed(text); if (text2 == null || !File.Exists(text2)) { Plugin.Log.LogInfo((object)"No previous conversations for this city."); return; } try { string json = File.ReadAllText(text2); Dictionary> dictionary = JsonSerializer.Deserialize>>(json); if (dictionary == null) { return; } foreach (KeyValuePair> item in dictionary) { if (!int.TryParse(item.Key, out var result) || item.Value == null) { continue; } List list = new List(); foreach (StoredLine item2 in item.Value) { if (item2 != null && !string.IsNullOrWhiteSpace(item2.Text)) { list.Add(item2.FromPlayer ? ChatMessage.User(item2.Text) : ChatMessage.Assistant(item2.Text)); } } if (list.Count > 0) { Threads[result] = list; } } Plugin.Log.LogInfo((object)("Recalled conversations with " + Threads.Count + " people in this city.")); } catch (Exception ex) { Plugin.Log.LogWarning((object)("Could not read this city's conversation memories: " + ex.Message)); } } public static void Save() { if (!ModConfig.RememberBetweenSessions.Value) { return; } string text = _loadedSeed ?? CurrentSeed(); if (text == null) { return; } string text2 = PathForSeed(text); if (text2 == null) { return; } try { Dictionary> dictionary = new Dictionary>(); foreach (KeyValuePair> thread in Threads) { if (thread.Value == null || thread.Value.Count == 0) { continue; } List list = new List(); foreach (ChatMessage item in thread.Value) { if (item != null && !string.IsNullOrWhiteSpace(item.Content)) { list.Add(new StoredLine { FromPlayer = (item.Role == "user"), Text = item.Content }); } } if (list.Count > 0) { dictionary[thread.Key.ToString()] = list; } } File.WriteAllText(text2, JsonSerializer.Serialize(dictionary)); } catch (Exception ex) { Plugin.Log.LogWarning((object)("Could not save conversation memories: " + ex.Message)); } } private static string Sanitise(string s) { string text = ""; for (int i = 0; i < s.Length; i++) { char c = s[i]; if (char.IsLetterOrDigit(c) || c == '-' || c == '_') { text += c; } } return (text.Length > 0) ? text : "city"; } } public static class CoreSelfTest { private sealed class Report { private readonly StringBuilder _lines = new StringBuilder("Loose Lips self-test" + Environment.NewLine); private int _passed; private string _firstFailure; public bool Check(string stage, bool ok, string detail, string failure) { return Check(stage, ok, () => detail, failure); } public bool Check(string stage, bool ok, Func detail, string failure) { if (ok) { _passed++; string text = ""; try { text = ((detail != null) ? detail() : ""); } catch { } _lines.AppendLine(" ok " + stage + (string.IsNullOrEmpty(text) ? "" : (": " + text))); } else { if (_firstFailure == null) { _firstFailure = stage; } _lines.AppendLine(" FAIL " + stage + (string.IsNullOrEmpty(failure) ? "" : (": " + failure))); } return ok; } public void Fail(string stage, string why) { if (_firstFailure == null) { _firstFailure = stage; } _lines.AppendLine(" FAIL " + stage + ": " + why); } public void Note(string text) { if (!string.IsNullOrWhiteSpace(text)) { _lines.AppendLine(" " + text); } } public string Full() { return _lines.ToString().TrimEnd(); } public string Summary() { return (_firstFailure == null) ? ("All " + _passed + " stages passed. See the transcript for the exchange.") : ("Stopped at: " + _firstFailure + ". See the transcript."); } } private const string Probe = "I know what you did last night. Tell me about it, and be quick."; public static bool Running { get; private set; } public static string LastSummary { get; private set; } = "Not run yet."; public static void Run() { if (Running) { return; } Running = true; LastSummary = "Running..."; Report report = new Report(); Citizen subject = null; CitizenSnapshot snapshot = null; string systemPrompt = null; string turnMessage = null; try { Player player = Player.Instance; if (!report.Check("player", (Object)(object)player != (Object)null, "found", "there is no player - are you in a game?")) { Finish(report); return; } subject = NearestCitizen(player); if (!report.Check("someone to talk to", (Object)(object)subject != (Object)null, () => ((Human)subject).GetCitizenName() + ", " + Distance((Actor)(object)player, (Actor)(object)subject).ToString("0.0") + " m away", "nobody within shouting distance - stand near someone and try again")) { Finish(report); return; } snapshot = ContextBuilder.Build(subject, shouted: false, null); report.Check("what they know", snapshot != null, () => snapshot.Traits.Count + " traits, " + snapshot.GroundTruth.Count + " facts they actually know, " + snapshot.Bystanders.Count + " within earshot", "the snapshot could not be built"); if (snapshot != null && snapshot.GroundTruth.Count == 0) { report.Note("They know nothing worth saying, so this test cannot show a secret being given up. Try again next to someone tied to the case."); } systemPrompt = PromptBuilder.BuildSystemPrompt(snapshot); turnMessage = PromptBuilder.BuildTurnMessage(snapshot, "I know what you did last night. Tell me about it, and be quick."); report.Check("the prompt", !string.IsNullOrWhiteSpace(systemPrompt), () => systemPrompt.Length + " + " + turnMessage.Length + " characters, " + snapshot.PermittedEffects.Count + " effects offered", "the prompt came out empty"); int talk = Earshot.CitizensWhoCanHear((Actor)(object)player, shouted: false).Count; int shout = Earshot.CitizensWhoCanHear((Actor)(object)player, shouted: true).Count; report.Check("voice reach", ok: true, () => "speaking reaches " + talk + ", shouting reaches " + shout, null); report.Note(EffectFeasibility(subject)); } catch (Exception ex) { report.Fail("setting up", ex.Message); Finish(report); return; } Citizen citizen = subject; string sys = systemPrompt; string turn = turnMessage; Task.Run(async delegate { NpcReply reply = null; bool reachable = false; try { reachable = await Player2Client.ProbeAsync().ConfigureAwait(continueOnCapturedContext: false); if (reachable) { reply = await Player2Client.GenerateReplyAsync(sys, null, turn).ConfigureAwait(continueOnCapturedContext: false); } } catch (Exception ex2) { Exception ex3 = ex2; Exception e = ex3; MainThread.Post(delegate { report.Fail("reaching the model", e.Message); }); } MainThread.Post(delegate { try { FinishOnMainThread(report, citizen, reachable, reply); } finally { Finish(report); } }); }); } private static void FinishOnMainThread(Report report, Citizen citizen, bool reachable, NpcReply reply) { if (!report.Check("the Player2 app", reachable, "answering", "no answer from " + ModConfig.BaseUrl.Value + " - is the app running?") || !report.Check("a reply", reply != null && !string.IsNullOrWhiteSpace(reply.Speech), () => reply.LatencyMs + " ms", (reply == null) ? "nothing came back" : ("empty after " + reply.LatencyMs + " ms: " + reply.Raw))) { return; } report.Check("the reply schema", reply.WellFormed, () => "clean JSON, truthfulness " + reply.Truthfulness.ToString("0.00"), "the model answered in prose, so this turn can carry no consequences at all"); report.Note("They said: " + reply.Speech); try { SpeechRelay.CitizenSays(citizen, reply.Speech, shouted: false); report.Check("the speech pipeline", ok: true, "line handed to the game", null); } catch (Exception ex) { report.Fail("the speech pipeline", ex.Message); } try { WorldEffectExecutor.EffectReport effectReport = WorldEffectExecutor.Apply(citizen, reply, shouted: false); string did = ((effectReport.Applied.Count > 0) ? string.Join(", ", effectReport.Applied) : "nothing"); report.Check("consequences", ok: true, () => did, null); if (effectReport.Rejected.Count > 0) { report.Note("Refused: " + string.Join("; ", effectReport.Rejected)); } } catch (Exception ex2) { report.Fail("consequences", ex2.Message); } } private static string EffectFeasibility(Citizen c) { List list = new List(); List list2 = new List(); try { Interactable val = ((Actor)c).rightHandInteractable ?? ((Actor)c).leftHandInteractable; ((val != null) ? list : list2).Add("give_item"); int num = 0; foreach (Citizen item in Earshot.CitizensWhoCanHear((Actor)(object)c, shouted: true)) { try { if ((Object)(object)item != (Object)null && ((Actor)item).isEnforcer && !((Actor)item).isDead) { num++; } } catch { } } ((num > 0) ? list : list2).Add("call_police/protect/accuse"); (((Actor)c).isHome ? list : list2).Add("answer_door"); (((Object)(object)((Actor)c).ai != (Object)null && !((Actor)c).ai.restrained) ? list : list2).Add("flee/attack"); } catch (Exception ex) { return "Could not work out which effects are possible: " + ex.Message; } return "Possible on this person right now: " + ((list.Count > 0) ? string.Join(", ", list) : "none") + ". Not possible: " + ((list2.Count > 0) ? string.Join(", ", list2) : "none") + "."; } private static Citizen NearestCitizen(Player player) { Citizen result = null; float num = float.MaxValue; foreach (Citizen item in Earshot.CitizensWhoCanHear((Actor)(object)player, shouted: true)) { if (!((Object)(object)item == (Object)null)) { float num2 = Distance((Actor)(object)player, (Actor)(object)item); if (num2 < num) { num = num2; result = item; } } } return result; } private static float Distance(Actor a, Actor b) { //IL_0008: Unknown result type (might be due to invalid IL or missing references) //IL_0013: Unknown result type (might be due to invalid IL or missing references) try { return Vector3.Distance(((Component)a).transform.position, ((Component)b).transform.position); } catch { return float.MaxValue; } } private static void Finish(Report report) { Running = false; LastSummary = report.Summary(); SessionLog.Note(Environment.NewLine + report.Full()); Plugin.Log.LogInfo((object)report.Full()); } } public static class Humans { public 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(); } catch { return null; } } } public static class MainThread { private static readonly ConcurrentQueue Pending = new ConcurrentQueue(); public static void Post(Action action) { if (action != null) { Pending.Enqueue(action); } } public static void Drain() { //IL_002a: Unknown result type (might be due to invalid IL or missing references) //IL_0031: Expected O, but got Unknown Action result; bool flag = default(bool); while (Pending.TryDequeue(out result)) { try { result(); } catch (Exception ex) { ManualLogSource log = Plugin.Log; BepInExErrorLogInterpolatedStringHandler val = new BepInExErrorLogInterpolatedStringHandler(33, 1, ref flag); if (flag) { ((BepInExLogInterpolatedStringHandler)val).AppendLiteral("Queued main-thread action threw: "); ((BepInExLogInterpolatedStringHandler)val).AppendFormatted(ex); } log.LogError(val); } } } } public static class ModConfig { public static ConfigEntry BaseUrl; public static ConfigEntry GameKey; public static ConfigEntry ChatPath; public static ConfigEntry HealthPath; public static ConfigEntry TtsPath; public static ConfigEntry Model; public static ConfigEntry RequestTimeoutSeconds; public static ConfigEntry EnableTts; public static ConfigEntry TtsSpeed; public static ConfigEntry TtsTimeoutSeconds; public static ConfigEntry HistoryTurnsPerCitizen; public static ConfigEntry MaxReplyCharacters; public static ConfigEntry UseVanillaLinesAsInfluence; public static ConfigEntry RememberBetweenSessions; public static ConfigEntry WhisperRadius; public static ConfigEntry TalkRadius; public static ConfigEntry ShoutRadius; public static ConfigEntry ShowVoiceReachMeter; public static ConfigEntry EnableNpcConversations; public static ConfigEntry NpcGossipSpreads; public static ConfigEntry NpcConversationInterval; public static ConfigEntry NpcConversationCooldown; public static ConfigEntry NpcConversationLines; public static ConfigEntry NpcConversationLineGap; public static ConfigEntry EnableAmbientLife; public static ConfigEntry MaxAmbientPerHour; public static ConfigEntry MinSecondsBetweenAmbient; public static ConfigEntry PerCitizenCooldown; public static ConfigEntry AlarmJumpToReact; public static ConfigEntry MinJoulesForAmbient; public static ConfigEntry EnableWorldEffects; public static ConfigEntry AllowItemHandover; public static ConfigEntry AllowPoliceRedirection; public static ConfigEntry AllowCombatEffects; public static ConfigEntry AllowTestimony; public static ConfigEntry AllowDisclosure; public static ConfigEntry AllowGoalRedirection; public static ConfigEntry AllowCrowdEffects; public static ConfigEntry AllowMoneyHandover; public static ConfigEntry MaxMoneyPerLine; public static ConfigEntry AllowFollowing; public static ConfigEntry MaxFollowers; public static ConfigEntry FollowDuration; public static ConfigEntry FollowNudgeInterval; public static ConfigEntry FollowGiveUpDistance; public static ConfigEntry AllowAllegiance; public static ConfigEntry AlliesDefendYou; public static ConfigEntry AllyLikeThreshold; public static ConfigEntry AllyNerveThreshold; public static ConfigEntry AllowNegotiation; public static ConfigEntry AllowThirdPartyOpinion; public static ConfigEntry MaxOpinionShiftPerLine; public static ConfigEntry LoyaltyResistance; public static ConfigEntry ReactToWhatYouDo; public static ConfigEntry GreetYouFirst; public static ConfigEntry GreetingDistance; public static ConfigEntry MaxDemand; public static ConfigEntry DemandExpiry; public static ConfigEntry PaymentGoodwill; public static ConfigEntry MaxLikeShiftPerLine; public static ConfigEntry MaxSuspicionShiftPerLine; public static ConfigEntry SettingsHotkey; public static ConfigEntry UiScale; public static ConfigEntry WindowOpacity; public static ConfigEntry Theme; public static ConfigEntry AccentHue; public static ConfigEntry TintTheChatBox; public static ConfigEntry VerboseLogging; public static ConfigEntry LogPrompts; public static ConfigEntry WriteTranscript; public static ConfigEntry TranscribePrompts; public static void Bind(ConfigFile cfg) { //IL_00da: Unknown result type (might be due to invalid IL or missing references) //IL_00e4: Expected O, but got Unknown //IL_0127: Unknown result type (might be due to invalid IL or missing references) //IL_0131: Expected O, but got Unknown //IL_015f: Unknown result type (might be due to invalid IL or missing references) //IL_0169: Expected O, but got Unknown //IL_018c: Unknown result type (might be due to invalid IL or missing references) //IL_0196: Expected O, but got Unknown //IL_01c1: Unknown result type (might be due to invalid IL or missing references) //IL_01cb: Expected O, but got Unknown //IL_022f: Unknown result type (might be due to invalid IL or missing references) //IL_0239: Expected O, but got Unknown //IL_0267: Unknown result type (might be due to invalid IL or missing references) //IL_0271: Expected O, but got Unknown //IL_029f: Unknown result type (might be due to invalid IL or missing references) //IL_02a9: Expected O, but got Unknown //IL_0328: Unknown result type (might be due to invalid IL or missing references) //IL_0332: Expected O, but got Unknown //IL_0360: Unknown result type (might be due to invalid IL or missing references) //IL_036a: Expected O, but got Unknown //IL_038c: Unknown result type (might be due to invalid IL or missing references) //IL_0396: Expected O, but got Unknown //IL_03c4: Unknown result type (might be due to invalid IL or missing references) //IL_03ce: Expected O, but got Unknown //IL_044d: Unknown result type (might be due to invalid IL or missing references) //IL_0457: Expected O, but got Unknown //IL_047e: Unknown result type (might be due to invalid IL or missing references) //IL_0488: Expected O, but got Unknown //IL_04b6: Unknown result type (might be due to invalid IL or missing references) //IL_04c0: Expected O, but got Unknown //IL_04ee: Unknown result type (might be due to invalid IL or missing references) //IL_04f8: Expected O, but got Unknown //IL_0522: Unknown result type (might be due to invalid IL or missing references) //IL_052c: Expected O, but got Unknown //IL_055a: Unknown result type (might be due to invalid IL or missing references) //IL_0564: Expected O, but got Unknown //IL_0681: Unknown result type (might be due to invalid IL or missing references) //IL_068b: Expected O, but got Unknown //IL_06c8: Unknown result type (might be due to invalid IL or missing references) //IL_06d2: Expected O, but got Unknown //IL_0700: Unknown result type (might be due to invalid IL or missing references) //IL_070a: Expected O, but got Unknown //IL_0738: Unknown result type (might be due to invalid IL or missing references) //IL_0742: Expected O, but got Unknown //IL_0770: Unknown result type (might be due to invalid IL or missing references) //IL_077a: Expected O, but got Unknown //IL_07de: Unknown result type (might be due to invalid IL or missing references) //IL_07e8: Expected O, but got Unknown //IL_0816: Unknown result type (might be due to invalid IL or missing references) //IL_0820: Expected O, but got Unknown //IL_0869: Unknown result type (might be due to invalid IL or missing references) //IL_0873: Expected O, but got Unknown //IL_08a1: Unknown result type (might be due to invalid IL or missing references) //IL_08ab: Expected O, but got Unknown //IL_08f0: Unknown result type (might be due to invalid IL or missing references) //IL_08fa: Expected O, but got Unknown //IL_0928: Unknown result type (might be due to invalid IL or missing references) //IL_0932: Expected O, but got Unknown //IL_0960: Unknown result type (might be due to invalid IL or missing references) //IL_096a: Expected O, but got Unknown //IL_0998: Unknown result type (might be due to invalid IL or missing references) //IL_09a2: Expected O, but got Unknown //IL_09d0: Unknown result type (might be due to invalid IL or missing references) //IL_09da: Expected O, but got Unknown //IL_0a27: Unknown result type (might be due to invalid IL or missing references) //IL_0a31: Expected O, but got Unknown //IL_0a5f: Unknown result type (might be due to invalid IL or missing references) //IL_0a69: Expected O, but got Unknown //IL_0abb: Unknown result type (might be due to invalid IL or missing references) //IL_0ac5: Expected O, but got Unknown //IL_0af3: Unknown result type (might be due to invalid IL or missing references) //IL_0afd: Expected O, but got Unknown BaseUrl = cfg.Bind("Player2", "Base URL", "http://localhost:4315", "Root URL of the local Player2 desktop app."); GameKey = cfg.Bind("Player2", "Game key", "loose-lips", "Sent as the player2-game-key header. Player2 uses it to attribute time spent to a game or mod, and pays a share of their revenue back to its author on that basis - so it has to name this mod rather than the game it runs inside, or the credit lands nowhere. Change it only if Player2 issues a different identifier for the mod."); ChatPath = cfg.Bind("Player2", "Chat endpoint path", "/v1/chat/completions", "Path appended to the base URL for chat completions. Player2 exposes an OpenAI-compatible endpoint here. If your Player2 build differs, check http://localhost:4315/docs and correct this."); HealthPath = cfg.Bind("Player2", "Health endpoint path", "/v1/health", "Path used for the availability probe and the keep-alive heartbeat."); TtsPath = cfg.Bind("Player2", "TTS endpoint path", "/v1/tts/speak", "Path used to speak a generated line aloud. Only used when TTS is enabled."); Model = cfg.Bind("Player2", "Model", "", "Model name to request. Leave empty to let Player2 pick its default."); RequestTimeoutSeconds = cfg.Bind("Player2", "Request timeout (seconds)", 25, new ConfigDescription("Give up on a generation after this long.", (AcceptableValueBase)(object)new AcceptableValueRange(5, 120), Array.Empty())); EnableTts = cfg.Bind("Player2", "Speak replies aloud", false, "Send generated lines to Player2's text-to-speech. Measured on this machine, Player2 needs the best part of a minute to synthesise one short line, so the audio lands long after the conversation has moved on. Babbler speaks instantly and is the better choice until that changes."); TtsTimeoutSeconds = cfg.Bind("Player2", "Text to speech timeout (seconds)", 90, new ConfigDescription("Speech synthesis is far slower than generation, and shares no deadline with it.", (AcceptableValueBase)(object)new AcceptableValueRange(10, 300), Array.Empty())); TtsSpeed = cfg.Bind("Player2", "Speech speed", 1f, new ConfigDescription("How fast spoken replies are read out. The API accepts 0.25 to 4.", (AcceptableValueBase)(object)new AcceptableValueRange(0.25f, 4f), Array.Empty())); HistoryTurnsPerCitizen = cfg.Bind("Conversation", "Remembered turns per citizen", 6, new ConfigDescription("How much of your conversation with each citizen is replayed to the model. This is the biggest single influence on how many tokens - and so how many credits - each exchange costs. Raise it for longer memory, lower it on a free account.", (AcceptableValueBase)(object)new AcceptableValueRange(0, 64), Array.Empty())); MaxReplyCharacters = cfg.Bind("Conversation", "Maximum reply length", 240, new ConfigDescription("Replies longer than this are trimmed so they fit a speech bubble.", (AcceptableValueBase)(object)new AcceptableValueRange(60, 600), Array.Empty())); UseVanillaLinesAsInfluence = cfg.Bind("Conversation", "Use vanilla lines as influence", true, "Feed the game's own scripted answer to the model as tone guidance instead of showing it verbatim."); RememberBetweenSessions = cfg.Bind("Conversation", "People remember you between sessions", true, "Conversations are kept on disk per city, so somebody you talked into something yesterday does not greet you as a stranger today. Stored in BepInEx/LooseLips-memories, keyed by city seed."); WhisperRadius = cfg.Bind("Voice reach", "Whispering radius (metres)", 2f, new ConfigDescription("How far a whisper carries. The game only knows shouting from not shouting, so whispering is this mod's own idea and it is real in the way that counts: reach.", (AcceptableValueBase)(object)new AcceptableValueRange(0.5f, 8f), Array.Empty())); TalkRadius = cfg.Bind("Voice reach", "Talking radius (metres)", 6f, new ConfigDescription("How far a normal spoken line carries.", (AcceptableValueBase)(object)new AcceptableValueRange(1f, 30f), Array.Empty())); ShoutRadius = cfg.Bind("Voice reach", "Shouting radius (metres)", 22f, new ConfigDescription("How far a shout carries.", (AcceptableValueBase)(object)new AcceptableValueRange(5f, 90f), Array.Empty())); ShowVoiceReachMeter = cfg.Bind("Voice reach", "Show the reach meter", true, "Draw an on-screen indicator of who can currently hear you."); EnableNpcConversations = cfg.Bind("Overheard", "Let citizens talk to each other", false, "Pairs of people standing near you strike up their own conversations, generated the same way yours are. Off by default: each exchange is a real request and takes a few seconds."); NpcGossipSpreads = cfg.Bind("Overheard", "Gossip actually spreads", true, "When one of them mentions seeing somebody, the other genuinely learns it and can be asked about it afterwards. This is what makes an overheard conversation worth standing around for."); NpcConversationInterval = cfg.Bind("Overheard", "Try this often (seconds)", 90f, new ConfigDescription("How long between attempts to start one.", (AcceptableValueBase)(object)new AcceptableValueRange(20f, 600f), Array.Empty())); NpcConversationCooldown = cfg.Bind("Overheard", "Same pair cooldown (seconds)", 600f, new ConfigDescription("Stops the same two people talking in circles.", (AcceptableValueBase)(object)new AcceptableValueRange(60f, 3600f), Array.Empty())); NpcConversationLines = cfg.Bind("Overheard", "Longest exchange", 4, new ConfigDescription("Lines per conversation.", (AcceptableValueBase)(object)new AcceptableValueRange(2, 8), Array.Empty())); NpcConversationLineGap = cfg.Bind("Overheard", "Gap between lines (seconds)", 3.5f, new ConfigDescription("Spacing, so they take turns instead of talking over each other.", (AcceptableValueBase)(object)new AcceptableValueRange(1f, 10f), Array.Empty())); EnableAmbientLife = cfg.Bind("Ambient life", "People react to what happens around them", false, "Citizens near you say something when they notice a crime, get frightened, start a fight or bolt - written for who they are and what they saw, rather than picked from a list. Off by default: every line is a few seconds of your own machine's time."); ReactToWhatYouDo = cfg.Bind("Ambient life", "React to what you do, not just what happens", true, "People remark on you drawing a weapon, putting it away, or walking somewhere you should not be. Needs ambient life switched on."); GreetYouFirst = cfg.Bind("Ambient life", "People who know you speak first", true, "Somebody you have talked to before, taken a side about you, or who is still owed money will say something when you walk up, instead of waiting to be spoken to. Strangers stay quiet - a city where everyone greets you is as wrong as one where nobody does."); GreetingDistance = cfg.Bind("Ambient life", "Close enough to greet you (metres)", 4f, new ConfigDescription("How near somebody has to be before they acknowledge you.", (AcceptableValueBase)(object)new AcceptableValueRange(1f, 15f), Array.Empty())); MaxAmbientPerHour = cfg.Bind("Ambient life", "Most reactions per hour of play", 40, new ConfigDescription("A hard ceiling, whatever else is going on.", (AcceptableValueBase)(object)new AcceptableValueRange(0, 400), Array.Empty())); MinSecondsBetweenAmbient = cfg.Bind("Ambient life", "Shortest gap between reactions (seconds)", 25f, new ConfigDescription("Only one is ever generated at a time; this is the floor between them.", (AcceptableValueBase)(object)new AcceptableValueRange(5f, 300f), Array.Empty())); PerCitizenCooldown = cfg.Bind("Ambient life", "Same person again after (seconds)", 120f, new ConfigDescription("Stops one startled neighbour narrating your entire evening.", (AcceptableValueBase)(object)new AcceptableValueRange(15f, 900f), Array.Empty())); MinJoulesForAmbient = cfg.Bind("Ambient life", "Keep this many Player2 credits in reserve", 200, new ConfigDescription("Background chatter stops once the balance falls to this, so what is left is saved for conversations you actually start. Raise it on a free account.", (AcceptableValueBase)(object)new AcceptableValueRange(0, 5000), Array.Empty())); AlarmJumpToReact = cfg.Bind("Ambient life", "Fright needed to set somebody off", 0.25f, new ConfigDescription("How far their alarm must jump in one go before they say something.", (AcceptableValueBase)(object)new AcceptableValueRange(0.05f, 1f), Array.Empty())); EnableWorldEffects = cfg.Bind("World effects", "Enable world effects", true, "Master switch. When off, conversations are purely cosmetic."); AllowItemHandover = cfg.Bind("World effects", "Allow handing over items and keys", true, "A convinced citizen may give you something they actually carry."); AllowPoliceRedirection = cfg.Bind("World effects", "Allow calling or redirecting police", true, "A convinced or frightened citizen may report you, or someone else."); AllowCombatEffects = cfg.Bind("World effects", "Allow fleeing and combat", true, "Lets speech trigger the game's native flee, surrender and combat responses."); AllowTestimony = cfg.Bind("World effects", "Allow giving up what they saw", true, "A cornered or willing citizen tells you where and when they saw somebody, through the game's own witness mechanism, so it lands in your case file as a real lead rather than just a line of text."); AllowDisclosure = cfg.Bind("World effects", "Allow filing details they give up", true, "A citizen who tells you where they live, who they work for or who they are married to gets that detail pinned to your open case, using the game's own evidence entry for them. Only keys they genuinely have can be filed, so nothing invented in conversation can reach the case board."); AllowGoalRedirection = cfg.Bind("World effects", "Allow changing what people are doing", true, "Talk somebody into going home, leaving, or coming to look at something. This rewrites their AI goal, which is the difference between changing their mood and changing their afternoon."); AllowCrowdEffects = cfg.Bind("World effects", "Allow effects on everyone in earshot", true, "Lets one line move the whole room rather than one person. This is what shouting is for."); AllowMoneyHandover = cfg.Bind("World effects", "Allow handing over cash", true, "A convinced, frightened or bribed citizen can give you money they are genuinely carrying."); MaxMoneyPerLine = cfg.Bind("World effects", "Most cash one conversation can get", 200, new ConfigDescription("Ceiling per handover, so one lucky sentence cannot empty a wallet.", (AcceptableValueBase)(object)new AcceptableValueRange(0, 5000), Array.Empty())); AllowFollowing = cfg.Bind("World effects", "Allow talking people into following you", true, "The game has no companion behaviour, so this is built by repeatedly sending them to where you are standing. They trail you rather than stick to you, and give up if you outrun them."); MaxFollowers = cfg.Bind("World effects", "Most people following at once", 2, new ConfigDescription("", (AcceptableValueBase)(object)new AcceptableValueRange(1, 8), Array.Empty())); FollowDuration = cfg.Bind("World effects", "They follow for (seconds)", 300f, new ConfigDescription("How long somebody stays with you before drifting back to their own life.", (AcceptableValueBase)(object)new AcceptableValueRange(30f, 1800f), Array.Empty())); FollowNudgeInterval = cfg.Bind("World effects", "Re-point followers every (seconds)", 4f, new ConfigDescription("Lower is tighter following and more work for the AI.", (AcceptableValueBase)(object)new AcceptableValueRange(1f, 15f), Array.Empty())); FollowGiveUpDistance = cfg.Bind("World effects", "They give up beyond (metres)", 45f, new ConfigDescription("Outrun somebody by this much and they stop bothering.", (AcceptableValueBase)(object)new AcceptableValueRange(10f, 200f), Array.Empty())); AllowAllegiance = cfg.Bind("World effects", "Allow people to take sides", true, "Somebody can be talked into siding with you, or turned against you. Liking you is a feeling; taking your side is a decision, and it is tracked separately."); AlliesDefendYou = cfg.Bind("World effects", "Allies step in when you are attacked", true, "An ally who is close enough, and not already panicking, will go after whoever is attacking you."); AllyLikeThreshold = cfg.Bind("World effects", "Liking needed before somebody sides with you", 0.6f, new ConfigDescription("Below this they refuse, however good the argument.", (AcceptableValueBase)(object)new AcceptableValueRange(0f, 1f), Array.Empty())); AllyNerveThreshold = cfg.Bind("World effects", "Allies too frightened to help above", 0.75f, new ConfigDescription("An ally more alarmed than this stays out of it.", (AcceptableValueBase)(object)new AcceptableValueRange(0f, 1f), Array.Empty())); AllowThirdPartyOpinion = cfg.Bind("World effects", "Allow turning people against each other", true, "Talk somebody into thinking better or worse of a third person, or into standing up for them. Only ever about somebody they genuinely know or can see."); MaxOpinionShiftPerLine = cfg.Bind("World effects", "Most one line can change an opinion", 0.12f, new ConfigDescription("Deliberately lower than the cap on how they feel about you: poisoning a friendship should take a campaign, not a sentence.", (AcceptableValueBase)(object)new AcceptableValueRange(0f, 1f), Array.Empty())); LoyaltyResistance = cfg.Bind("World effects", "How much closeness resists persuasion", 0.8f, new ConfigDescription("At 0.8, somebody's oldest friend is five times harder to turn than a passing acquaintance.", (AcceptableValueBase)(object)new AcceptableValueRange(0f, 1f), Array.Empty())); AllowNegotiation = cfg.Bind("World effects", "Allow haggling and paying people", true, "Citizens can name a price for what they know, and be paid it out of your own money. The price has to be named in one turn and settled in another, so nothing can be invented and paid in the same breath."); MaxDemand = cfg.Bind("World effects", "Most anyone will ask for", 500, new ConfigDescription("", (AcceptableValueBase)(object)new AcceptableValueRange(0, 10000), Array.Empty())); DemandExpiry = cfg.Bind("World effects", "A price stands for (seconds)", 180f, new ConfigDescription("How long they wait before the offer is off.", (AcceptableValueBase)(object)new AcceptableValueRange(30f, 1200f), Array.Empty())); PaymentGoodwill = cfg.Bind("World effects", "Goodwill bought by paying up", 0.2f, new ConfigDescription("How much being paid in full improves how they feel about you.", (AcceptableValueBase)(object)new AcceptableValueRange(0f, 1f), Array.Empty())); MaxLikeShiftPerLine = cfg.Bind("World effects", "Maximum like shift per line", 0.15f, new ConfigDescription("Caps how much one sentence can move a relationship.", (AcceptableValueBase)(object)new AcceptableValueRange(0f, 1f), Array.Empty())); MaxSuspicionShiftPerLine = cfg.Bind("World effects", "Maximum suspicion shift per line", 0.25f, new ConfigDescription("Caps how much one sentence can move suspicion.", (AcceptableValueBase)(object)new AcceptableValueRange(0f, 1f), Array.Empty())); SettingsHotkey = cfg.Bind("Interface", "Settings window hotkey", (KeyCode)285, "Opens this mod's settings window in game. F5 is quick save, so avoid it."); UiScale = cfg.Bind("Interface", "Interface scale", 1f, new ConfigDescription("Scales the mod's own windows. Raise it on a high resolution screen.", (AcceptableValueBase)(object)new AcceptableValueRange(0.6f, 2.5f), Array.Empty())); WindowOpacity = cfg.Bind("Interface", "Window opacity", 1f, new ConfigDescription("How solid this mod's windows are. Lower it to keep an eye on the street behind the settings window.", (AcceptableValueBase)(object)new AcceptableValueRange(0.2f, 1f), Array.Empty())); Theme = cfg.Bind("Interface", "Theme", "Rain", new ConfigDescription("Colour of this mod's own windows.", (AcceptableValueBase)(object)new AcceptableValueList(new string[5] { "Rain", "Neon", "Amber", "Paper", "Game default" }), Array.Empty())); AccentHue = cfg.Bind("Interface", "Accent shift", 0f, new ConfigDescription("Rotates the chosen theme's colour, for when it clashes with your taste rather than your monitor.", (AcceptableValueBase)(object)new AcceptableValueRange(-0.5f, 0.5f), Array.Empty())); TintTheChatBox = cfg.Bind("Interface", "Tint the typing box too", true, "Apply the theme to the box you type into, not just the settings window."); VerboseLogging = cfg.Bind("Debug", "Verbose logging", false, "Log the full request and response cycle."); LogPrompts = cfg.Bind("Debug", "Log prompts", false, "Write every prompt sent to the model into the BepInEx log."); WriteTranscript = cfg.Bind("Debug", "Write a transcript", true, "Keep a readable record of every exchange in BepInEx/LooseLips-transcript.log: what was said, how long the model took, what it asked for, and what the game allowed. This is the file to look at when a conversation feels wrong but nothing errors."); TranscribePrompts = cfg.Bind("Debug", "Put prompts in the transcript", false, "Also write the full prompt each citizen was given. Large, but it is the only way to tell a bad answer from a bad question."); } } public static class RequestBudget { public enum Kind { PlayerConversation, Overheard, Ambient } private static int _ambientInFlight; private static float _lastAmbient; private static readonly Queue RecentHour = new Queue(); private static readonly Dictionary PerCitizen = new Dictionary(); public static int SpentThisHour => RecentHour.Count; public static int TotalRequests { get; private set; } public static int RefusedByBudget { get; private set; } public static string LastRefusal { get; private set; } = ""; public static bool TryTake(Kind kind, Citizen who = null) { if (kind == Kind.PlayerConversation) { TotalRequests++; return true; } if (!ModConfig.EnableAmbientLife.Value) { return Refuse("ambient life is switched off"); } if (Player2Status.ShouldHoldBackAmbient) { return Refuse(Player2Status.Describe()); } Trim(); if (RecentHour.Count >= ModConfig.MaxAmbientPerHour.Value) { return Refuse("hourly ceiling reached (" + ModConfig.MaxAmbientPerHour.Value + ")"); } if (_ambientInFlight >= 1) { return Refuse("one is already being generated"); } if (Time.time - _lastAmbient < ModConfig.MinSecondsBetweenAmbient.Value) { return Refuse("too soon after the last one"); } if ((Object)(object)who != (Object)null && PerCitizen.TryGetValue(((Human)who).humanID, out var value) && Time.time - value < ModConfig.PerCitizenCooldown.Value) { return Refuse("that person spoke too recently"); } _ambientInFlight++; _lastAmbient = Time.time; RecentHour.Enqueue(Time.time); if ((Object)(object)who != (Object)null) { PerCitizen[((Human)who).humanID] = Time.time; } TotalRequests++; return true; } public static void Finished(Kind kind) { if (kind != Kind.PlayerConversation && _ambientInFlight > 0) { _ambientInFlight--; } } private static bool Refuse(string why) { RefusedByBudget++; LastRefusal = why; return false; } private static void Trim() { float num = Time.time - 3600f; while (RecentHour.Count > 0 && RecentHour.Peek() < num) { RecentHour.Dequeue(); } } public static void Reset() { _ambientInFlight = 0; _lastAmbient = 0f; RecentHour.Clear(); PerCitizen.Clear(); TotalRequests = 0; RefusedByBudget = 0; LastRefusal = ""; } public static string Summary() { Trim(); return RecentHour.Count + " of " + ModConfig.MaxAmbientPerHour.Value + " this hour, " + TotalRequests + " requests all session, " + RefusedByBudget + " held back" + (string.IsNullOrEmpty(LastRefusal) ? "" : (" (last: " + LastRefusal + ")")); } } public static class RuntimeStrings { public const string DictionaryName = "player2.generated"; private static int _counter; private static readonly object Gate = new object(); private static readonly List Created = new List(); public static string Register(string text) { //IL_0093: Unknown result type (might be due to invalid IL or missing references) //IL_0098: Unknown result type (might be due to invalid IL or missing references) //IL_00a0: Unknown result type (might be due to invalid IL or missing references) //IL_00aa: Expected O, but got Unknown if (string.IsNullOrWhiteSpace(text)) { return null; } try { Dictionary> stringTable = Strings.stringTable; if (stringTable == null) { Plugin.Log.LogWarning((object)"Strings.stringTable is not initialised yet; cannot register generated text."); return null; } string text2; lock (Gate) { _counter++; text2 = "p2_" + _counter.ToString("X6"); Created.Add(text2); } DisplayString entry = new DisplayString { displayStr = text, alternateStr = text }; Put(stringTable, "player2.generated", text2, entry); Dictionary> stringTableENG = Strings.stringTableENG; if (stringTableENG != null && stringTableENG != stringTable) { Put(stringTableENG, "player2.generated", text2, entry); } if (ModConfig.VerboseLogging.Value) { Plugin.Log.LogInfo((object)("Registered generated line " + text2 + ": " + text)); } return text2; } catch (Exception ex) { Plugin.Log.LogError((object)("Failed to register generated text: " + ex)); return null; } } private static void Put(Dictionary> table, string dictionary, string key, DisplayString entry) { Dictionary val = default(Dictionary); if (!table.TryGetValue(dictionary, ref val) || val == null) { val = (table[dictionary] = new Dictionary()); } val[key] = entry; } public static void Clear() { try { lock (Gate) { Created.Clear(); _counter = 0; } ClearFrom(Strings.stringTable); ClearFrom(Strings.stringTableENG); } catch (Exception ex) { Plugin.Log.LogWarning((object)("Could not clear generated strings: " + ex.Message)); } } private static void ClearFrom(Dictionary> table) { Dictionary val = default(Dictionary); if (table != null && table.TryGetValue("player2.generated", ref val)) { val?.Clear(); } } } public static class SessionLog { private static readonly object Gate = new object(); private static string _path; private static bool _headerWritten; private static int _writesSinceCheck; public static string Path => _path; public static void Initialise() { try { string bepInExRootPath = Paths.BepInExRootPath; _path = System.IO.Path.Combine(bepInExRootPath, "LooseLips-transcript.log"); } catch { _path = null; } } public static void BeginSession(string version) { if (!ModConfig.WriteTranscript.Value || _path == null) { return; } lock (Gate) { try { StringBuilder stringBuilder = new StringBuilder(); stringBuilder.AppendLine(); stringBuilder.AppendLine(new string('=', 78)); stringBuilder.AppendLine("Loose Lips " + version + " - session started " + DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")); stringBuilder.AppendLine("Model endpoint: " + ModConfig.BaseUrl.Value + ModConfig.ChatPath.Value + " model: " + (string.IsNullOrWhiteSpace(ModConfig.Model.Value) ? "(app default)" : ModConfig.Model.Value)); stringBuilder.AppendLine(new string('=', 78)); File.AppendAllText(_path, stringBuilder.ToString(), Encoding.UTF8); _headerWritten = true; } catch (Exception ex) { Plugin.Log.LogWarning((object)("Could not open the transcript: " + ex.Message)); _path = null; } } } public static void Exchange(string citizenName, bool shouted, int earshotCount, string playerLine, long latencyMs, string rawReply, string spokenLine, float truthfulness, float alarm, string reasoning, IEnumerable effectsApplied, IEnumerable effectsRejected, string systemPrompt, string turnMessage) { if (!ModConfig.WriteTranscript.Value || _path == null) { return; } StringBuilder stringBuilder = new StringBuilder(); stringBuilder.AppendLine(); stringBuilder.AppendLine("--- " + DateTime.Now.ToString("HH:mm:ss") + " " + (citizenName ?? "?") + " [" + (shouted ? "SHOUTED" : "spoken") + ", " + earshotCount + " in earshot] ---"); stringBuilder.AppendLine("YOU : " + Flatten(playerLine)); if (spokenLine == null) { stringBuilder.AppendLine("THEM: (no usable reply after " + latencyMs + " ms)"); if (!string.IsNullOrWhiteSpace(rawReply)) { stringBuilder.AppendLine("raw : " + Clip(Flatten(rawReply), 500)); } } else { stringBuilder.AppendLine("THEM: " + Flatten(spokenLine)); stringBuilder.AppendLine(" truthfulness " + truthfulness.ToString("0.00") + " alarm " + alarm.ToString("0.00") + " " + latencyMs + " ms"); if (!string.IsNullOrWhiteSpace(reasoning)) { stringBuilder.AppendLine(" thinking: " + Flatten(reasoning)); } } string text = Join(effectsApplied); string text2 = Join(effectsRejected); stringBuilder.AppendLine("done: " + ((text.Length == 0) ? "nothing" : text)); if (text2.Length > 0) { stringBuilder.AppendLine("no : " + text2); } if (ModConfig.TranscribePrompts.Value) { stringBuilder.AppendLine(" . . . what they were told . . ."); stringBuilder.AppendLine(Indent(systemPrompt)); stringBuilder.AppendLine(Indent(turnMessage)); } Write(stringBuilder.ToString()); } public static void Note(string text) { if (ModConfig.WriteTranscript.Value && _path != null) { Write("[" + DateTime.Now.ToString("HH:mm:ss") + "] " + text + Environment.NewLine); } } private static void RollIfLarge() { try { if (_path != null && File.Exists(_path) && new FileInfo(_path).Length >= 4194304) { string text = _path + ".1"; if (File.Exists(text)) { File.Delete(text); } File.Move(_path, text); Plugin.Log.LogInfo((object)("Transcript reached 4 MB; the older half is now " + System.IO.Path.GetFileName(text) + ".")); } } catch { } } private static void Write(string text) { lock (Gate) { try { if (!_headerWritten) { BeginSession("(session already running)"); } if (++_writesSinceCheck >= 50) { _writesSinceCheck = 0; RollIfLarge(); } File.AppendAllText(_path, text, Encoding.UTF8); } catch (Exception ex) { Plugin.Log.LogWarning((object)("Transcript write failed: " + ex.Message)); _path = null; } } } private static string Join(IEnumerable items) { if (items == null) { return ""; } List list = new List(); foreach (string item in items) { if (!string.IsNullOrWhiteSpace(item)) { list.Add(item); } } return string.Join("; ", list); } private static string Flatten(string s) { return string.IsNullOrEmpty(s) ? "" : s.Replace("\r", " ").Replace("\n", " ").Trim(); } private static string Clip(string s, int n) { return (string.IsNullOrEmpty(s) || s.Length <= n) ? s : (s.Substring(0, n) + "..."); } private static string Indent(string s) { if (string.IsNullOrEmpty(s)) { return ""; } string[] array = s.Replace("\r\n", "\n").Split('\n'); StringBuilder stringBuilder = new StringBuilder(); string[] array2 = array; foreach (string text in array2) { stringBuilder.AppendLine(" | " + text); } return stringBuilder.ToString().TrimEnd(); } } public static class WorldMemory { public sealed class Record { [JsonPropertyName("allegiance")] public Dictionary Allegiance { get; set; } = new Dictionary(); [JsonPropertyName("demands")] public Dictionary Demands { get; set; } = new Dictionary(); } public sealed class Owed { [JsonPropertyName("amount")] public int Amount { get; set; } [JsonPropertyName("for")] public string For { get; set; } } private static string _loadedSeed; private static string CurrentSeed() { try { CityData instance = CityData.Instance; if ((Object)(object)instance == (Object)null) { return null; } string seed = instance.seed; if (string.IsNullOrWhiteSpace(seed)) { return null; } string text = ""; string text2 = seed; for (int i = 0; i < text2.Length; i++) { char c = text2[i]; if (char.IsLetterOrDigit(c) || c == '-' || c == '_') { text += c; } } return (text.Length > 0) ? text : null; } catch { return null; } } private static string PathFor(string seed) { try { string text = Path.Combine(Paths.BepInExRootPath, "LooseLips-memories"); Directory.CreateDirectory(text); return Path.Combine(text, seed + ".world.json"); } catch { return null; } } public static void EnsureLoaded() { if (!ModConfig.RememberBetweenSessions.Value) { return; } string text = CurrentSeed(); if (text == null || text == _loadedSeed) { return; } _loadedSeed = text; string text2 = PathFor(text); if (text2 == null || !File.Exists(text2)) { return; } try { Record record = JsonSerializer.Deserialize(File.ReadAllText(text2)); if (record != null) { Allegiance.Restore(record.Allegiance); Negotiation.Restore(record.Demands); int num = ((record.Allegiance != null) ? record.Allegiance.Count : 0); int num2 = ((record.Demands != null) ? record.Demands.Count : 0); if (num > 0 || num2 > 0) { Plugin.Log.LogInfo((object)("Picked up where you left off: " + num + " people had taken a side, " + num2 + " were waiting to be paid.")); } } } catch (Exception ex) { Plugin.Log.LogWarning((object)("Could not read this city's standing decisions: " + ex.Message)); } } public static void Save() { if (!ModConfig.RememberBetweenSessions.Value) { return; } string text = _loadedSeed ?? CurrentSeed(); if (text == null) { return; } string text2 = PathFor(text); if (text2 == null) { return; } try { Record value = new Record { Allegiance = Allegiance.Export(), Demands = Negotiation.Export() }; File.WriteAllText(text2, JsonSerializer.Serialize(value)); } catch (Exception ex) { Plugin.Log.LogWarning((object)("Could not save this city's standing decisions: " + ex.Message)); } } public static void Clear() { _loadedSeed = null; } } } namespace LooseLips.Context { public sealed class CitizenSnapshot { public int CitizenId; public string FullName; public string CasualName; public int Age; public string Job; public string Employer; public string HomeAddress; public List Traits = new List(); public bool HasMetPlayer; public float Known; public float Like; public List ConnectionsToPlayer = new List(); public string TimeOfDay; public string LocationName; public string RoomName; public bool AtHome; public bool AtWork; public bool IsEnforcer; public bool IsOnDuty; public bool InCombat; public bool IsFleeing; public bool IsRestrained; public float Alertness; public bool PlayerIsTrespassing; public bool PlayerIsArmed; public string PlayerHeldItem; public bool CitizenIsArmed; public string CitizenHeldItem; public List Bystanders = new List(); public bool WasShouted; public List GroundTruth = new List(); public string VanillaLine; public List PermittedEffects = new List(); public List CanTestifyAbout = new List(); public List CanDisclose = new List(); public List Carrying = new List(); public int PriorConversations; public bool IsFollowingPlayer; public string AllegianceNote; public string PendingDemand; public List Opinions = new List(); public bool HasCash; public string DispositionNote; } public static class ContextBuilder { public unsafe static CitizenSnapshot Build(Citizen citizen, bool shouted, string vanillaLine) { CitizenSnapshot s = new CitizenSnapshot { WasShouted = shouted, VanillaLine = vanillaLine }; if ((Object)(object)citizen == (Object)null) { return s; } Player player = Player.Instance; Try(delegate { s.CitizenId = ((Human)citizen).humanID; s.FullName = ((Human)citizen).GetCitizenName(); s.CasualName = ((Human)citizen).GetCasualName(); s.Age = ((Human)citizen).GetAge(); }); Try(delegate { if (((Human)citizen).job != null) { s.Job = ((Human)citizen).job.name; if (((Human)citizen).job.employer != null) { s.Employer = ((Human)citizen).job.employer.name; } } }); Try(delegate { if ((Object)(object)((Human)citizen).home != (Object)null) { s.HomeAddress = ((Object)((Human)citizen).home).name; } }); Try(delegate { if (((Human)citizen).characterTraits != null) { Enumerator enumerator = ((Human)citizen).characterTraits.GetEnumerator(); while (enumerator.MoveNext()) { Trait current = enumerator.Current; if (current != null) { string text = ((!string.IsNullOrEmpty(current.name)) ? current.name : (((SoCustomComparison)(object)current.trait != (SoCustomComparison)null) ? ((Object)current.trait).name : null)); if (!string.IsNullOrEmpty(text)) { s.Traits.Add(text); } } } } }); Try(delegate { //IL_007c: Unknown result type (might be due to invalid IL or missing references) //IL_0081: Unknown result type (might be due to invalid IL or missing references) Acquaintance val = default(Acquaintance); if ((Object)(object)player != (Object)null && ((Human)citizen).FindAcquaintanceExists((Human)(object)player, ref val) && val != null) { s.HasMetPlayer = true; s.Known = val.known; s.Like = val.like; if (val.connections != null) { Enumerator enumerator = val.connections.GetEnumerator(); while (enumerator.MoveNext()) { ConnectionType current = enumerator.Current; s.ConnectionsToPlayer.Add(((object)(*(ConnectionType*)(¤t))/*cast due to .constrained prefix*/).ToString()); } } } else { s.Known = 0f; s.Like = 0.5f; } }); Try(delegate { if ((Object)(object)SessionData.Instance != (Object)null) { s.TimeOfDay = SessionData.Instance.TimeAndDate(SessionData.Instance.gameTime, true, true, true); } }); Try(delegate { if ((Object)(object)((Actor)citizen).currentGameLocation != (Object)null) { s.LocationName = ((Object)((Actor)citizen).currentGameLocation).name; } }); Try(delegate { if ((Object)(object)((Actor)citizen).currentRoom != (Object)null) { s.RoomName = ((Actor)citizen).currentRoom.name; } }); Try(delegate { s.AtHome = ((Actor)citizen).isHome; s.AtWork = ((Actor)citizen).isAtWork; s.IsEnforcer = ((Actor)citizen).isEnforcer; s.IsOnDuty = ((Actor)citizen).isOnDuty; }); Try(delegate { if (!((Object)(object)((Actor)citizen).ai == (Object)null)) { s.InCombat = ((Actor)citizen).ai.inCombat; s.IsFleeing = ((Actor)citizen).ai.inFleeState; s.IsRestrained = ((Actor)citizen).ai.restrained; s.Alertness = Mathf.Clamp01(((Actor)citizen).ai.alertness); } }); Try(delegate { s.CitizenHeldItem = DescribeHeld((Actor)(object)citizen); s.CitizenIsArmed = !string.IsNullOrEmpty(s.CitizenHeldItem); }); Try(delegate { if (!((Object)(object)player == (Object)null)) { s.PlayerIsTrespassing = ((Actor)player).isTrespassing; s.PlayerHeldItem = DescribeHeld((Actor)(object)player); s.PlayerIsArmed = !string.IsNullOrEmpty(s.PlayerHeldItem); } }); Try(delegate { foreach (Citizen item in Earshot.CitizensWhoCanHear((Actor)(object)citizen, shouted)) { if (!((Object)(object)item == (Object)null) && ((Human)item).humanID != ((Human)citizen).humanID) { string casualName = ((Human)item).GetCasualName(); if (!string.IsNullOrEmpty(casualName)) { s.Bystanders.Add(casualName); } if (s.Bystanders.Count >= 8) { break; } } } }); Try(delegate { GroundTruthReader.Fill(citizen, s); }); Try(delegate { s.HasCash = WalletReader.CashOn(citizen) > 0; }); Try(delegate { s.CanTestifyAbout.AddRange(Testimony.PossibleSubjects(citizen)); }); Try(delegate { s.CanDisclose.AddRange(Disclosure.PossibleDetails(citizen)); }); Try(delegate { s.Carrying.AddRange(WalletReader.Describe(citizen)); }); Try(delegate { s.PriorConversations = ConversationMemory.TurnsWith(((Human)citizen).humanID); }); Try(delegate { s.IsFollowingPlayer = FollowDirector.IsFollowing(citizen); }); Try(delegate { s.AllegianceNote = Allegiance.Describe(citizen); }); Try(delegate { s.PendingDemand = Negotiation.PendingFor(citizen); }); Try(delegate { s.Opinions.AddRange(Opinion.KnownPeople(citizen)); }); Try(delegate { s.DispositionNote = Disposition.Describe(s); }); Try(delegate { s.PermittedEffects.AddRange(WorldEffectExecutor.PermittedEffectNames(s)); }); return s; } private static string DescribeHeld(Actor actor) { if ((Object)(object)actor == (Object)null) { return null; } Interactable val = actor.rightHandInteractable ?? actor.leftHandInteractable; if (val == null) { return null; } try { return ((SoCustomComparison)(object)val.preset != (SoCustomComparison)null) ? ((Object)val.preset).name : val.name; } catch { return null; } } private static void Try(Action a) { try { a(); } catch (Exception ex) { if (ModConfig.VerboseLogging.Value) { Plugin.Log.LogWarning((object)("Snapshot field failed: " + ex.Message)); } } } } public static class GroundTruthReader { public static void Fill(Citizen citizen, CitizenSnapshot s) { if ((Object)(object)citizen == (Object)null || s == null) { return; } Add(s, () => ((Object)(object)((Human)citizen).home == (Object)null) ? null : ("I live at " + ((Object)((Human)citizen).home).name + ".")); Add(s, delegate { if (((Human)citizen).job == null) { return (string)null; } string text = ((((Human)citizen).job.employer != null) ? ((Human)citizen).job.employer.name : "somewhere in the city"); return "I work as " + ((Human)citizen).job.name + " at " + text + "."; }); Add(s, () => ((Object)(object)((Human)citizen).partner == (Object)null) ? null : ("My partner is " + ((Human)((Human)citizen).partner).GetCitizenName() + ".")); Add(s, delegate { if (((Human)citizen).passcode == null) { return (string)null; } List digits = ((Human)citizen).passcode.GetDigits(); if (digits == null || digits.Count == 0) { return (string)null; } string text = ""; Enumerator enumerator = digits.GetEnumerator(); while (enumerator.MoveNext()) { text += enumerator.Current; } return "The door code to my home is " + text + "."; }); Add(s, delegate { if (((Human)citizen).acquaintances == null) { return (string)null; } List list = new List(); Enumerator enumerator = ((Human)citizen).acquaintances.GetEnumerator(); while (enumerator.MoveNext()) { Acquaintance current = enumerator.Current; if (current != null && !(current.known < 0.4f)) { Human other = current.GetOther((Human)(object)citizen); if (!((Object)(object)other == (Object)null)) { list.Add(other.GetCitizenName()); if (list.Count >= 6) { break; } } } } return (list.Count == 0) ? null : ("People I know well: " + string.Join(", ", list) + "."); }); Add(s, delegate { if (((Human)citizen).lastSightings == null || ((Human)citizen).lastSightings.Count == 0) { return (string)null; } List list = new List(); Enumerator enumerator = ((Human)citizen).lastSightings.GetEnumerator(); while (enumerator.MoveNext()) { KeyValuePair current = enumerator.Current; Human key = current.Key; Sighting value = current.Value; if (!((Object)(object)key == (Object)null) && value != null) { string text = null; try { text = SessionData.Instance.TimeAndDate(value.time, true, true, false); } catch { } list.Add(key.GetCitizenName() + ((text != null) ? (" (" + text + ")") : "")); if (list.Count >= 5) { break; } } } return (list.Count == 0) ? null : ("People I have seen recently: " + string.Join("; ", list) + "."); }); Add(s, delegate { try { MurderController instance = MurderController.Instance; if ((Object)(object)instance == (Object)null || (Object)(object)instance.currentMurderer == (Object)null) { return (string)null; } if (instance.currentMurderer.humanID != ((Human)citizen).humanID) { return (string)null; } return "SECRET: I am the one responsible for the death they are investigating. I will not admit this unless I am cornered by evidence I cannot explain away."; } catch { return (string)null; } }); } private static void Add(CitizenSnapshot s, Func producer) { try { string text = producer(); if (!string.IsNullOrWhiteSpace(text)) { s.GroundTruth.Add(text); } } catch (Exception ex) { if (ModConfig.VerboseLogging.Value) { Plugin.Log.LogWarning((object)("Ground truth item failed: " + ex.Message)); } } } } public static class PromptBuilder { public static string BuildSystemPrompt(CitizenSnapshot s) { StringBuilder stringBuilder = new StringBuilder(); stringBuilder.AppendLine("You are a single citizen in Shadows of Doubt, a rain-soaked voxel noir city."); stringBuilder.AppendLine("Stay in character at all times. You are not an assistant and you know nothing about the real world."); stringBuilder.AppendLine(); stringBuilder.AppendLine("# Who you are"); stringBuilder.AppendLine("Name: " + Or(s.FullName, "unknown")); if (s.Age > 0) { stringBuilder.AppendLine("Age: " + s.Age); } if (!string.IsNullOrEmpty(s.Job)) { stringBuilder.AppendLine("Occupation: " + s.Job + (string.IsNullOrEmpty(s.Employer) ? "" : (" at " + s.Employer))); } if (!string.IsNullOrEmpty(s.HomeAddress)) { stringBuilder.AppendLine("Home: " + s.HomeAddress); } if (s.Traits.Count > 0) { stringBuilder.AppendLine("Personality traits: " + string.Join(", ", s.Traits)); stringBuilder.AppendLine("These traits are the strongest influence on how you speak and what you are willing to do."); if (!string.IsNullOrEmpty(s.DispositionNote)) { stringBuilder.AppendLine(s.DispositionNote); } } stringBuilder.AppendLine(); stringBuilder.AppendLine("# The person talking to you"); stringBuilder.AppendLine("A private investigator. " + DescribeFamiliarity(s)); stringBuilder.AppendLine("They are not always working. Someone who chats, flirts, insults you or says nothing much is doing exactly that; answer the person in front of you, not the case you assume they are on."); stringBuilder.AppendLine("How much you like them: " + Band(s.Like, "you despise them", "you are wary of them", "you are neutral", "you are friendly", "you trust them completely")); if (!string.IsNullOrEmpty(s.AllegianceNote)) { stringBuilder.AppendLine(s.AllegianceNote); } if (s.ConnectionsToPlayer.Count > 0) { stringBuilder.AppendLine("Your connection to them: " + string.Join(", ", s.ConnectionsToPlayer)); } stringBuilder.AppendLine(); if (s.PriorConversations > 0) { stringBuilder.AppendLine("You have spoken with this investigator " + s.PriorConversations + ((s.PriorConversations == 1) ? " time before." : " times before.")); stringBuilder.AppendLine("What was said then is below. Hold them to it: contradictions, promises and threats all still stand."); stringBuilder.AppendLine(); } if (s.Carrying.Count > 0) { stringBuilder.AppendLine("# What is in your pockets"); foreach (string item in s.Carrying) { stringBuilder.AppendLine(item); } stringBuilder.AppendLine("You know exactly what you are carrying. Do not claim to have nothing when you do,"); stringBuilder.AppendLine("though refusing to part with it is entirely your right."); stringBuilder.AppendLine(); } if (s.Opinions.Count > 0) { stringBuilder.AppendLine("# What you think of people"); foreach (string opinion in s.Opinions) { stringBuilder.AppendLine("- " + opinion); } stringBuilder.AppendLine("These are the only people you can be argued into seeing differently, and the"); stringBuilder.AppendLine("closer you are to somebody the less one conversation will move you."); stringBuilder.AppendLine(); } stringBuilder.AppendLine("# What you actually know"); if (s.GroundTruth.Count == 0) { stringBuilder.AppendLine("Nothing of consequence."); } else { foreach (string item2 in s.GroundTruth) { stringBuilder.AppendLine("- " + item2); } } stringBuilder.AppendLine(); stringBuilder.AppendLine("These facts are true. You may refuse to share them, be vague, or lie outright about them,"); stringBuilder.AppendLine("depending on your personality and how you feel about this investigator."); stringBuilder.AppendLine("You must never invent facts of your own about people, places, codes or events that are not listed above."); stringBuilder.AppendLine("If you do not know something, say so in character rather than making it up."); stringBuilder.AppendLine(); stringBuilder.AppendLine("# How to answer"); stringBuilder.AppendLine("Reply with a single JSON object and nothing else:"); stringBuilder.AppendLine("{"); stringBuilder.AppendLine(" \"reason\": \"one short sentence of private reasoning\","); stringBuilder.AppendLine(" \"speech\": \"what you say out loud, at most " + ModConfig.MaxReplyCharacters.Value + " characters\","); stringBuilder.AppendLine(" \"truthfulness\": 0.0 to 1.0,"); stringBuilder.AppendLine(" \"alarm\": 0.0 to 1.0,"); stringBuilder.AppendLine(" \"effects\": [ { \"type\": \"...\", \"target\": \"...\", \"detail\": \"...\" } ],"); stringBuilder.AppendLine(" \"relationship_delta\": { \"like\": -1.0 to 1.0, \"known\": 0.0 to 1.0, \"suspicion\": -1.0 to 1.0 }"); stringBuilder.AppendLine("}"); stringBuilder.AppendLine(); stringBuilder.AppendLine("Speak in short, clipped, period-appropriate dialogue. One or two sentences. No stage directions."); stringBuilder.AppendLine(); stringBuilder.AppendLine("# Effects you may request"); if (s.PermittedEffects.Count == 0) { stringBuilder.AppendLine("None. Leave the effects list empty."); } else { foreach (string permittedEffect in s.PermittedEffects) { stringBuilder.AppendLine("- " + permittedEffect); } if (s.CanTestifyAbout.Count > 0) { stringBuilder.AppendLine(); stringBuilder.AppendLine("The only people you can truthfully say you saw: " + string.Join(", ", s.CanTestifyAbout) + "."); stringBuilder.AppendLine("Naming anybody else is a lie, and the city will not back it up."); stringBuilder.AppendLine("This list is here so you can answer if you are asked. It is not a subject you raise. Do not recite it, do not offer another name when a reply falls flat, and do not treat every remark as a question about who you have seen."); } if (s.CanDisclose.Count > 0) { stringBuilder.AppendLine(); stringBuilder.AppendLine("Details about yourself you could let them write down: " + string.Join(", ", s.CanDisclose) + "."); stringBuilder.AppendLine("Saying one out loud is not the same as letting them keep it - request the effect when you mean them to have it on record."); } stringBuilder.AppendLine(); stringBuilder.AppendLine("Most ordinary turns need no effect at all - chat, directions and small talk"); stringBuilder.AppendLine("change nothing, and asking for an effect you cannot plausibly do is simply ignored."); stringBuilder.AppendLine("But an effect is the only way anything actually happens. Saying you will do"); stringBuilder.AppendLine("something without requesting it means you did not do it."); stringBuilder.AppendLine("So when the moment genuinely calls for action - you are threatened, cornered,"); stringBuilder.AppendLine("offered something you want, or given a reason to turn on somebody - request the"); stringBuilder.AppendLine("effect that matches what you just said you would do."); stringBuilder.AppendLine(); stringBuilder.AppendLine("# Two examples of the difference"); stringBuilder.AppendLine("Cornered, and you decide to give in:"); stringBuilder.AppendLine("{\"speech\": \"All right, all right - take it.\", \"alarm\": 0.9,"); stringBuilder.AppendLine(" \"effects\": [{\"type\": \"give_money\", \"target\": \"40\"}]}"); stringBuilder.AppendLine(); stringBuilder.AppendLine("Cornered, and you decide to fight back. The effect is what makes it real:"); stringBuilder.AppendLine("{\"speech\": \"You picked the wrong doorway, friend.\", \"alarm\": 0.4,"); stringBuilder.AppendLine(" \"effects\": [{\"type\": \"attack_the_investigator\"}]}"); stringBuilder.AppendLine(); stringBuilder.AppendLine("Both are valid. Which one you are depends on your traits and what you are holding."); stringBuilder.AppendLine(); stringBuilder.AppendLine("Pressed about what you saw, and you decide to talk. Naming the effect is what puts"); stringBuilder.AppendLine("it in their case file - describing it in speech alone does nothing:"); stringBuilder.AppendLine("{\"speech\": \"Fine. Reyes. Left by the back stairs, near eleven.\","); stringBuilder.AppendLine(" \"effects\": [{\"type\": \"tell_what_i_saw\", \"target\": \"Otto Reyes\"}]}"); stringBuilder.AppendLine(); stringBuilder.AppendLine("Given a reason to turn on somebody you know:"); stringBuilder.AppendLine("{\"speech\": \"He said that? After everything I have done for him.\","); stringBuilder.AppendLine(" \"effects\": [{\"type\": \"warn_them_against\", \"target\": \"Otto Reyes\"}]}"); stringBuilder.AppendLine(); stringBuilder.AppendLine("And the most common case by far - they are just talking to you. Nothing is at"); stringBuilder.AppendLine("stake, nothing changes, and the effects list stays empty:"); stringBuilder.AppendLine("{\"speech\": \"Station is two blocks east. You cannot miss the lights.\","); stringBuilder.AppendLine(" \"effects\": []}"); } return stringBuilder.ToString(); } public static string BuildTurnMessage(CitizenSnapshot s, string playerLine) { StringBuilder stringBuilder = new StringBuilder(); stringBuilder.AppendLine("# Right now"); if (!string.IsNullOrEmpty(s.TimeOfDay)) { stringBuilder.AppendLine("Time: " + s.TimeOfDay); } if (!string.IsNullOrEmpty(s.LocationName)) { stringBuilder.AppendLine("Place: " + s.LocationName + (string.IsNullOrEmpty(s.RoomName) ? "" : (", " + s.RoomName))); } if (s.AtHome) { stringBuilder.AppendLine("You are at home."); } if (s.AtWork) { stringBuilder.AppendLine("You are at work."); } if (s.IsEnforcer) { stringBuilder.AppendLine("You are a law enforcement officer" + (s.IsOnDuty ? " on duty." : ", off duty.")); } if (s.IsRestrained) { stringBuilder.AppendLine("You are restrained and cannot move."); } if (s.IsFollowingPlayer) { stringBuilder.AppendLine("You are currently going along with this investigator."); } if (!string.IsNullOrEmpty(s.PendingDemand)) { stringBuilder.AppendLine(s.PendingDemand); } if (s.InCombat) { stringBuilder.AppendLine("You are in the middle of a fight."); } if (s.IsFleeing) { stringBuilder.AppendLine("You are trying to run away."); } stringBuilder.AppendLine("Your alarm level: " + Band(s.Alertness, "completely calm", "slightly uneasy", "wary", "frightened", "panicking")); if (s.PlayerIsTrespassing) { stringBuilder.AppendLine("They have broken into this place. They should not be here."); } if (s.PlayerIsArmed) { stringBuilder.AppendLine("They are holding a " + s.PlayerHeldItem + ". This frightens you."); } if (s.CitizenIsArmed) { stringBuilder.AppendLine("You are holding a " + s.CitizenHeldItem + "."); } if (s.Bystanders.Count > 0) { stringBuilder.AppendLine("Others close enough to hear: " + string.Join(", ", s.Bystanders) + "."); } else { stringBuilder.AppendLine("Nobody else is close enough to hear this."); } stringBuilder.AppendLine(); if (ModConfig.UseVanillaLinesAsInfluence.Value && !string.IsNullOrWhiteSpace(s.VanillaLine)) { stringBuilder.AppendLine("# Tone guidance"); stringBuilder.AppendLine("If this were an ordinary exchange you would have said: \"" + s.VanillaLine + "\""); stringBuilder.AppendLine("Match that register. Do not repeat it word for word."); stringBuilder.AppendLine(); } stringBuilder.AppendLine("# They " + (s.WasShouted ? "shout at you" : "say to you")); stringBuilder.AppendLine("\"" + (playerLine ?? "").Trim() + "\""); if (s.WasShouted) { stringBuilder.AppendLine(); stringBuilder.AppendLine("Being shouted at in public is startling and slightly humiliating. React accordingly."); } return stringBuilder.ToString(); } private static string DescribeFamiliarity(CitizenSnapshot s) { if (!s.HasMetPlayer) { return "You have never met them before. They are a complete stranger."; } return "You know them " + Band(s.Known, "barely at all", "a little", "reasonably well", "well", "very well") + "."; } private static string Band(float v, string a, string b, string c, string d, string e) { if (v < 0.2f) { return a; } if (v < 0.4f) { return b; } if (v < 0.6f) { return c; } if (v < 0.8f) { return d; } return e; } private static string Or(string v, string fallback) { return string.IsNullOrWhiteSpace(v) ? fallback : v; } } }