Please disclose if any significant portion of your mod was created using AI tools by adding the 'AI Generated' category. Failing to do so may result in the mod being removed from Thunderstore.
Decompiled source of LastWords v1.0.1
plugins/LastWords/aj.LastWords.dll
Decompiled 2 days agousing System; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Linq; using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.Versioning; using BepInEx; using BepInEx.Configuration; using BepInEx.Logging; using Dissonance; using GameNetcodeStuff; using HarmonyLib; using LastWords.Configuration; using Microsoft.CodeAnalysis; using TMPro; using Unity.Collections; using Unity.Netcode; using UnityEngine; using UnityEngine.InputSystem; using UnityEngine.InputSystem.Controls; using UnityEngine.Rendering.HighDefinition; [assembly: CompilationRelaxations(8)] [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)] [assembly: TargetFramework(".NETStandard,Version=v2.1", FrameworkDisplayName = ".NET Standard 2.1")] [assembly: AssemblyCompany("aj.LastWords")] [assembly: AssemblyConfiguration("Release")] [assembly: AssemblyFileVersion("1.0.1.0")] [assembly: AssemblyInformationalVersion("1.0.1")] [assembly: AssemblyProduct("LastWords")] [assembly: AssemblyTitle("aj.LastWords")] [assembly: AssemblyMetadata("AI_Assisted_Creation", "This assembly was created with the assistance of generative AI (code); see the README credits for which assets.")] [assembly: AssemblyMetadata("AI_Model_Vendor", "Anthropic")] [assembly: AssemblyVersion("1.0.1.0")] [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 LastWords { internal class LastWordsTestKeys : MonoBehaviour { private const Key DieKey = (Key)99; public static void Install() { //IL_0005: Unknown result type (might be due to invalid IL or missing references) //IL_000a: Unknown result type (might be due to invalid IL or missing references) //IL_0012: Unknown result type (might be due to invalid IL or missing references) //IL_0018: Expected O, but got Unknown GameObject val = new GameObject("LastWordsTestKeys") { hideFlags = (HideFlags)61 }; Object.DontDestroyOnLoad((Object)val); val.AddComponent<LastWordsTestKeys>(); Plugin.Logger.LogInfo((object)"Test keys on: F6 drops you dead."); } private void Update() { //IL_004e: Unknown result type (might be due to invalid IL or missing references) //IL_0058: Unknown result type (might be due to invalid IL or missing references) //IL_005e: Unknown result type (might be due to invalid IL or missing references) Keyboard current = Keyboard.current; if (current != null && ((ButtonControl)current[(Key)99]).wasPressedThisFrame) { PlayerControllerB val = (((Object)(object)GameNetworkManager.Instance != (Object)null) ? GameNetworkManager.Instance.localPlayerController : null); if ((Object)(object)val != (Object)null && !val.isPlayerDead && val.isPlayerControlled) { val.KillPlayer(Vector3.up, true, (CauseOfDeath)0, 0, default(Vector3), false); } } } } [HarmonyPatch] internal static class Patches { [HarmonyPatch(typeof(StartOfRound), "Start")] [HarmonyPostfix] private static void StartOfRoundStarted(StartOfRound __instance) { SpiritNet.Register(); ((Component)__instance).gameObject.AddComponent<SpiritRunner>(); } [HarmonyPatch(typeof(StartOfRound), "UpdatePlayerVoiceEffects")] [HarmonyPostfix] private static void VoicesReset() { Spirit.ReapplyVoices(); } } [BepInPlugin("aj.LastWords", "LastWords", "1.0.1")] public class Plugin : BaseUnityPlugin { internal static ManualLogSource Logger; internal static GameObject? SpiritPrefab; internal static PluginConfig BoundConfig { get; private set; } private void Awake() { //IL_007e: Unknown result type (might be due to invalid IL or missing references) Logger = ((BaseUnityPlugin)this).Logger; BoundConfig = new PluginConfig(((BaseUnityPlugin)this).Config); AssetBundle val = AssetBundle.LoadFromFile(Path.Combine(Path.GetDirectoryName(((BaseUnityPlugin)this).Info.Location), "lastwordsassets")); if ((Object)(object)val == (Object)null) { Logger.LogError((object)"Failed to load the lastwordsassets bundle; spirits will have no glow."); } else { SpiritPrefab = val.LoadAsset<GameObject>("LastWordsSpirit"); } SpiritView.CreateKey(BoundConfig.SpeakKey.Value); new Harmony("aj.LastWords").PatchAll(typeof(Patches)); if (BoundConfig.TestKeys.Value) { LastWordsTestKeys.Install(); } Logger.LogInfo((object)"LastWords 1.0.1 loaded."); } } internal class Spirit { internal const int Head = 0; internal const int Chest = 5; private const float LateEndGrace = 2f; private static readonly Dictionary<int, Spirit> active = new Dictionary<int, Spirit>(); private readonly PlayerControllerB player; private readonly DeadBodyInfo body; private readonly float endsAt; private readonly SpiritGlow? glow; private SpiritVoice? voice; private bool muted; private static PlayerControllerB? Local { get { if (!((Object)(object)GameNetworkManager.Instance != (Object)null)) { return null; } return GameNetworkManager.Instance.localPlayerController; } } private Spirit(PlayerControllerB player, DeadBodyInfo body, float seconds) { this.player = player; this.body = body; endsAt = Time.time + seconds; glow = SpiritGlow.Spawn(player, body); } internal static DeadBodyInfo? BodyOf(PlayerControllerB? p) { DeadBodyInfo val = (((Object)(object)p != (Object)null && p.isPlayerDead) ? p.deadBody : null); if (!((Object)(object)val != (Object)null) || val.deactivated || !((Component)val).gameObject.activeInHierarchy || val.bodyParts.Length <= 5) { return null; } return val; } internal static void Begin(int index, float seconds) { PlayerControllerB[] allPlayerScripts = StartOfRound.Instance.allPlayerScripts; if (index < 0 || index >= allPlayerScripts.Length) { return; } Stop(index); PlayerControllerB val = allPlayerScripts[index]; DeadBodyInfo val2 = BodyOf(val); if (!((Object)(object)val2 == (Object)null)) { Spirit spirit = new Spirit(val, val2, seconds); active[index] = spirit; spirit.UpdateVoice(); if ((Object)(object)val == (Object)(object)Local) { SpiritView.Speaking(val2, seconds); } } } internal static void Finish(int index, float cooldown) { Stop(index); PlayerControllerB[] allPlayerScripts = StartOfRound.Instance.allPlayerScripts; if (index >= 0 && index < allPlayerScripts.Length && (Object)(object)allPlayerScripts[index] == (Object)(object)Local) { SpiritView.Resting(cooldown); } } internal static void Clear() { foreach (Spirit value in active.Values) { if ((Object)(object)value.glow != (Object)null) { Object.Destroy((Object)(object)((Component)value.glow).gameObject); } } active.Clear(); } private static void Stop(int index) { if (active.TryGetValue(index, out Spirit value)) { active.Remove(index); if ((Object)(object)value.glow != (Object)null) { value.glow.FadeOut(); } if (value.voice != null) { value.voice.Restore(); } if (value.muted) { StartOfRound.Instance.UpdatePlayerVoiceEffects(); } if ((Object)(object)value.player == (Object)(object)Local) { SpiritView.StoppedSpeaking(); } } } internal static void TickAll() { if (active.Count == 0) { return; } foreach (KeyValuePair<int, Spirit> item in active.ToList()) { Spirit value = item.Value; if (Time.time > value.endsAt + 2f || (Object)(object)BodyOf(value.player) != (Object)(object)value.body) { Stop(item.Key); } else { value.UpdateVoice(); } } } internal static void LateTickAll() { foreach (Spirit value in active.Values) { value.voice?.Place(); } } internal static void ReapplyVoices() { foreach (Spirit value in active.Values) { value.voice?.Apply(); if (value.muted) { value.Mute(); } } } private void UpdateVoice() { PlayerControllerB local = Local; int num; int num2; if ((Object)(object)local != (Object)null) { num = (((Object)(object)local != (Object)(object)player) ? 1 : 0); if (num != 0) { num2 = ((!local.isPlayerDead) ? 1 : 0); goto IL_002d; } } else { num = 0; } num2 = 0; goto IL_002d; IL_002d: bool flag = (byte)num2 != 0; bool num3 = num != 0 && local.isPlayerDead; if (voice != null && (!flag || !voice.IsCurrent)) { SpiritVoice? spiritVoice = voice; voice = null; spiritVoice.Restore(); } if (!num3 && muted) { muted = false; StartOfRound.Instance.UpdatePlayerVoiceEffects(); } if (flag && voice == null) { voice = SpiritVoice.Create(player, body); voice?.Apply(); } if (num3 && !muted && player.voicePlayerState != null) { muted = true; Mute(); } } private void Mute() { if (player.voicePlayerState != null) { player.voicePlayerState.Volume = 0f; } } } internal class SpiritGlow : MonoBehaviour { private const float FadeSeconds = 0.5f; private const float LightHeight = 0.3f; private const float SoulHeight = 0.3f; private const float BobHeight = 0.02f; private const float BobSpeed = 1.6f; private const float ThreadStart = 0.05f; private const float BreathDepth = 0.1f; private const float BreathSpeed = 1.8f; private const float Quiet = 0.005f; private const float Loud = 0.15f; private const float RiseSpeed = 18f; private const float FallSpeed = 7f; private const float SoulSwell = 0.6f; private const float SpriteSwell = 1.5f; private const float LightSwell = 1.2f; private PlayerControllerB speaker; private DeadBodyInfo body; private HDAdditionalLightData? lightData; private float lightIntensity; private Transform soul; private Transform thread; private Transform glowLight; private Transform[] soulSprites; private Vector3 soulScale; private Renderer[] sprites; private Color[] spriteColors; private MaterialPropertyBlock block; private float fade; private float voice; private bool ending; private static readonly int Emission = Shader.PropertyToID("_EmissiveColor"); internal static SpiritGlow? Spawn(PlayerControllerB speaker, DeadBodyInfo body) { if ((Object)(object)Plugin.SpiritPrefab == (Object)null) { return null; } SpiritGlow spiritGlow = Object.Instantiate<GameObject>(Plugin.SpiritPrefab).AddComponent<SpiritGlow>(); spiritGlow.Init(speaker, body); return spiritGlow; } private void Init(PlayerControllerB player, DeadBodyInfo deadBody) { //IL_0057: Unknown result type (might be due to invalid IL or missing references) //IL_005c: Unknown result type (might be due to invalid IL or missing references) //IL_0111: Unknown result type (might be due to invalid IL or missing references) //IL_0116: Unknown result type (might be due to invalid IL or missing references) //IL_012b: Unknown result type (might be due to invalid IL or missing references) //IL_0135: Expected O, but got Unknown speaker = player; body = deadBody; glowLight = ((Component)this).transform.Find("Glow"); soul = ((Component)this).transform.Find("Soul"); thread = ((Component)this).transform.Find("Thread"); soulScale = soul.localScale; soulSprites = (Transform[])(object)new Transform[soul.childCount]; for (int i = 0; i < soulSprites.Length; i++) { soulSprites[i] = soul.GetChild(i); } lightData = ((Component)glowLight).GetComponent<HDAdditionalLightData>(); lightIntensity = (((Object)(object)lightData != (Object)null) ? lightData.intensity : 0f); sprites = ((Component)this).GetComponentsInChildren<Renderer>(); spriteColors = (Color[])(object)new Color[sprites.Length]; for (int j = 0; j < sprites.Length; j++) { spriteColors[j] = sprites[j].sharedMaterial.GetColor(Emission); } block = new MaterialPropertyBlock(); Follow(); } internal void FadeOut() { ending = true; } private void LateUpdate() { if ((Object)(object)body == (Object)null) { ending = true; } fade = Mathf.MoveTowards(fade, ending ? 0f : 1f, Time.deltaTime / 0.5f); if (ending && fade <= 0f) { Object.Destroy((Object)(object)((Component)this).gameObject); return; } float num = ((ending || (Object)(object)speaker == (Object)null) ? 0f : Mathf.Sqrt(Mathf.InverseLerp(0.005f, 0.15f, SpiritVoice.Loudness(speaker)))); voice = Mathf.Lerp(voice, num, 1f - Mathf.Exp((0f - ((num > voice) ? 18f : 7f)) * Time.deltaTime)); if ((Object)(object)body != (Object)null) { Follow(); } } private void Follow() { //IL_000d: Unknown result type (might be due to invalid IL or missing references) //IL_0012: Unknown result type (might be due to invalid IL or missing references) //IL_0019: Unknown result type (might be due to invalid IL or missing references) //IL_0025: Unknown result type (might be due to invalid IL or missing references) //IL_0026: 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_0035: Unknown result type (might be due to invalid IL or missing references) //IL_0045: Unknown result type (might be due to invalid IL or missing references) //IL_0046: 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_006c: Unknown result type (might be due to invalid IL or missing references) //IL_007d: Unknown result type (might be due to invalid IL or missing references) //IL_0094: Unknown result type (might be due to invalid IL or missing references) //IL_0127: Unknown result type (might be due to invalid IL or missing references) //IL_0147: Unknown result type (might be due to invalid IL or missing references) //IL_01a6: Unknown result type (might be due to invalid IL or missing references) //IL_01ab: Unknown result type (might be due to invalid IL or missing references) //IL_01bf: Unknown result type (might be due to invalid IL or missing references) //IL_01c4: Unknown result type (might be due to invalid IL or missing references) //IL_01c5: Unknown result type (might be due to invalid IL or missing references) //IL_01ca: Unknown result type (might be due to invalid IL or missing references) //IL_01e2: Unknown result type (might be due to invalid IL or missing references) //IL_01e3: Unknown result type (might be due to invalid IL or missing references) //IL_01ed: Unknown result type (might be due to invalid IL or missing references) //IL_01f2: Unknown result type (might be due to invalid IL or missing references) //IL_01f7: Unknown result type (might be due to invalid IL or missing references) //IL_01ff: Unknown result type (might be due to invalid IL or missing references) //IL_0204: Unknown result type (might be due to invalid IL or missing references) //IL_0206: Unknown result type (might be due to invalid IL or missing references) //IL_020b: Unknown result type (might be due to invalid IL or missing references) //IL_0213: Unknown result type (might be due to invalid IL or missing references) //IL_0215: Unknown result type (might be due to invalid IL or missing references) //IL_021c: Unknown result type (might be due to invalid IL or missing references) //IL_0221: Unknown result type (might be due to invalid IL or missing references) //IL_0231: Unknown result type (might be due to invalid IL or missing references) //IL_0236: Unknown result type (might be due to invalid IL or missing references) //IL_0237: Unknown result type (might be due to invalid IL or missing references) //IL_023e: Unknown result type (might be due to invalid IL or missing references) //IL_0243: Unknown result type (might be due to invalid IL or missing references) //IL_0248: Unknown result type (might be due to invalid IL or missing references) //IL_0272: Unknown result type (might be due to invalid IL or missing references) //IL_0277: 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_028d: Unknown result type (might be due to invalid IL or missing references) //IL_0294: Unknown result type (might be due to invalid IL or missing references) //IL_025e: Unknown result type (might be due to invalid IL or missing references) //IL_0260: Unknown result type (might be due to invalid IL or missing references) //IL_0262: Unknown result type (might be due to invalid IL or missing references) Vector3 position = body.bodyParts[5].position; ((Component)this).transform.position = position; glowLight.position = position + Vector3.up * 0.3f; soul.position = position + Vector3.up * (0.3f + 0.02f * Mathf.Sin(Time.time * 1.6f)); soul.localScale = soulScale * (1f + 0.6f * voice); float num = 1f - 0.1f * (1f - voice) * 0.5f * (1f + Mathf.Sin(Time.time * 1.8f)); if ((Object)(object)lightData != (Object)null) { lightData.intensity = lightIntensity * fade * num * (1f + 1.2f * voice); } for (int i = 0; i < sprites.Length; i++) { block.SetColor(Emission, spriteColors[i] * (fade * num * (1f + 1.5f * voice))); sprites[i].SetPropertyBlock(block); } Camera val = (((Object)(object)StartOfRound.Instance != (Object)null) ? StartOfRound.Instance.activeCamera : Camera.main); if (!((Object)(object)val == (Object)null)) { Vector3 position2 = ((Component)val).transform.position; Transform[] array = soulSprites; foreach (Transform obj in array) { obj.rotation = Quaternion.LookRotation(obj.position - position2); } Vector3 val2 = position + Vector3.up * 0.05f; Vector3 val3 = soul.position - val2; thread.position = val2 + val3 * 0.5f; Vector3 val4 = Vector3.ProjectOnPlane(thread.position - position2, ((Vector3)(ref val3)).normalized); if (((Vector3)(ref val4)).sqrMagnitude > 1E-06f) { thread.rotation = Quaternion.LookRotation(val4, val3); } Vector3 localScale = thread.localScale; thread.localScale = new Vector3(localScale.x, ((Vector3)(ref val3)).magnitude, localScale.z); } } } internal static class SpiritNet { internal enum Refusal : byte { NoBody, Speaking, Resting } [CompilerGenerated] private static class <>O { public static HandleNamedMessageDelegate <0>__OnRequest; public static HandleNamedMessageDelegate <1>__OnStart; public static HandleNamedMessageDelegate <2>__OnEnd; public static HandleNamedMessageDelegate <3>__OnRefused; } private const string RequestMessage = "aj.LastWords.Request"; private const string StartMessage = "aj.LastWords.Start"; private const string EndMessage = "aj.LastWords.End"; private const string RefusedMessage = "aj.LastWords.Refused"; private static readonly Dictionary<int, float> speakingUntil = new Dictionary<int, float>(); private static readonly Dictionary<int, float> restingUntil = new Dictionary<int, float>(); private static CustomMessagingManager? Messages { get { if (!((Object)(object)NetworkManager.Singleton != (Object)null)) { return null; } return NetworkManager.Singleton.CustomMessagingManager; } } private static bool IsHost { get { if ((Object)(object)NetworkManager.Singleton != (Object)null) { return NetworkManager.Singleton.IsServer; } return false; } } internal static void Register() { //IL_0043: 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: Expected O, but got Unknown //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_0074: Expected O, but got Unknown //IL_008f: Unknown result type (might be due to invalid IL or missing references) //IL_0094: Unknown result type (might be due to invalid IL or missing references) //IL_009a: Expected O, but got Unknown //IL_00b5: Unknown result type (might be due to invalid IL or missing references) //IL_00ba: Unknown result type (might be due to invalid IL or missing references) //IL_00c0: Expected O, but got Unknown speakingUntil.Clear(); restingUntil.Clear(); CustomMessagingManager messages = Messages; if (messages == null) { Plugin.Logger.LogError((object)"No network messaging; Last Words is off for this lobby."); return; } object obj = <>O.<0>__OnRequest; if (obj == null) { HandleNamedMessageDelegate val = OnRequest; <>O.<0>__OnRequest = val; obj = (object)val; } messages.RegisterNamedMessageHandler("aj.LastWords.Request", (HandleNamedMessageDelegate)obj); object obj2 = <>O.<1>__OnStart; if (obj2 == null) { HandleNamedMessageDelegate val2 = OnStart; <>O.<1>__OnStart = val2; obj2 = (object)val2; } messages.RegisterNamedMessageHandler("aj.LastWords.Start", (HandleNamedMessageDelegate)obj2); object obj3 = <>O.<2>__OnEnd; if (obj3 == null) { HandleNamedMessageDelegate val3 = OnEnd; <>O.<2>__OnEnd = val3; obj3 = (object)val3; } messages.RegisterNamedMessageHandler("aj.LastWords.End", (HandleNamedMessageDelegate)obj3); object obj4 = <>O.<3>__OnRefused; if (obj4 == null) { HandleNamedMessageDelegate val4 = OnRefused; <>O.<3>__OnRefused = val4; obj4 = (object)val4; } messages.RegisterNamedMessageHandler("aj.LastWords.Refused", (HandleNamedMessageDelegate)obj4); } internal static void Unregister() { CustomMessagingManager messages = Messages; if (messages != null) { messages.UnregisterNamedMessageHandler("aj.LastWords.Request"); messages.UnregisterNamedMessageHandler("aj.LastWords.Start"); messages.UnregisterNamedMessageHandler("aj.LastWords.End"); messages.UnregisterNamedMessageHandler("aj.LastWords.Refused"); } speakingUntil.Clear(); restingUntil.Clear(); } internal unsafe static void Ask(int player) { //IL_002a: 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_003e: Unknown result type (might be due to invalid IL or missing references) if (IsHost) { HostRequest(player, 0uL); return; } CustomMessagingManager messages = Messages; if (messages == null) { return; } FastBufferWriter val = default(FastBufferWriter); ((FastBufferWriter)(ref val))..ctor(4, (Allocator)2, -1); try { ((FastBufferWriter)(ref val)).WriteValueSafe<int>(ref player, default(ForPrimitives)); messages.SendNamedMessage("aj.LastWords.Request", 0uL, val, (NetworkDelivery)2); } finally { ((IDisposable)(*(FastBufferWriter*)(&val))/*cast due to .constrained prefix*/).Dispose(); } } private static void OnRequest(ulong sender, FastBufferReader reader) { //IL_000e: Unknown result type (might be due to invalid IL or missing references) //IL_0014: Unknown result type (might be due to invalid IL or missing references) if (IsHost) { int player = default(int); ((FastBufferReader)(ref reader)).ReadValueSafe<int>(ref player, default(ForPrimitives)); HostRequest(player, sender); } } private static void HostRequest(int player, ulong sender) { PlayerControllerB[] allPlayerScripts = StartOfRound.Instance.allPlayerScripts; if (player < 0 || player >= allPlayerScripts.Length) { return; } PlayerControllerB val = allPlayerScripts[player]; if (val.actualClientId == sender && val.isPlayerDead && !StartOfRound.Instance.shipIsLeaving) { if ((Object)(object)Spirit.BodyOf(val) == (Object)null) { Refuse(sender, Refusal.NoBody, 0f); return; } if (speakingUntil.ContainsKey(player)) { Refuse(sender, Refusal.Speaking, 0f); return; } if (restingUntil.TryGetValue(player, out var value) && Time.time < value) { Refuse(sender, Refusal.Resting, value - Time.time); return; } PluginConfig boundConfig = Plugin.BoundConfig; float num = Mathf.Max(1f, boundConfig.MinSeconds.Value); float num2 = Random.Range(num, Mathf.Max(num, boundConfig.MaxSeconds.Value)); speakingUntil[player] = Time.time + num2; Broadcast("aj.LastWords.Start", player, num2); } } internal static void HostTick() { if (!IsHost || speakingUntil.Count == 0) { return; } PlayerControllerB[] allPlayerScripts = StartOfRound.Instance.allPlayerScripts; foreach (int item in speakingUntil.Keys.ToList()) { PlayerControllerB p = allPlayerScripts[item]; if (!(Time.time < speakingUntil[item]) || !((Object)(object)Spirit.BodyOf(p) != (Object)null) || StartOfRound.Instance.shipIsLeaving) { speakingUntil.Remove(item); float num = Mathf.Max(0f, Plugin.BoundConfig.CooldownSeconds.Value); restingUntil[item] = Time.time + num; Broadcast("aj.LastWords.End", item, num); } } } private static void OnStart(ulong sender, FastBufferReader reader) { //IL_000a: 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_001c: Unknown result type (might be due to invalid IL or missing references) //IL_0022: Unknown result type (might be due to invalid IL or missing references) if (sender == 0L) { int index = default(int); ((FastBufferReader)(ref reader)).ReadValueSafe<int>(ref index, default(ForPrimitives)); float seconds = default(float); ((FastBufferReader)(ref reader)).ReadValueSafe<float>(ref seconds, default(ForPrimitives)); Spirit.Begin(index, seconds); } } private static void OnEnd(ulong sender, FastBufferReader reader) { //IL_000a: 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_001c: Unknown result type (might be due to invalid IL or missing references) //IL_0022: Unknown result type (might be due to invalid IL or missing references) if (sender == 0L) { int index = default(int); ((FastBufferReader)(ref reader)).ReadValueSafe<int>(ref index, default(ForPrimitives)); float cooldown = default(float); ((FastBufferReader)(ref reader)).ReadValueSafe<float>(ref cooldown, default(ForPrimitives)); Spirit.Finish(index, cooldown); } } private static void OnRefused(ulong sender, FastBufferReader reader) { //IL_000a: 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_001c: Unknown result type (might be due to invalid IL or missing references) //IL_0022: Unknown result type (might be due to invalid IL or missing references) if (sender == 0L) { byte why = default(byte); ((FastBufferReader)(ref reader)).ReadValueSafe<byte>(ref why, default(ForPrimitives)); float resting = default(float); ((FastBufferReader)(ref reader)).ReadValueSafe<float>(ref resting, default(ForPrimitives)); SpiritView.Refused((Refusal)why, resting); } } private unsafe static void Broadcast(string message, int player, float value) { //IL_0056: Unknown result type (might be due to invalid IL or missing references) //IL_005c: 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_006e: Unknown result type (might be due to invalid IL or missing references) //IL_0077: Unknown result type (might be due to invalid IL or missing references) CustomMessagingManager messages = Messages; List<ulong> list = NetworkManager.Singleton.ConnectedClientsIds.Where((ulong id) => id != 0).ToList(); if (messages != null && list.Count > 0) { FastBufferWriter val = default(FastBufferWriter); ((FastBufferWriter)(ref val))..ctor(8, (Allocator)2, -1); try { ((FastBufferWriter)(ref val)).WriteValueSafe<int>(ref player, default(ForPrimitives)); ((FastBufferWriter)(ref val)).WriteValueSafe<float>(ref value, default(ForPrimitives)); messages.SendNamedMessage(message, (IReadOnlyList<ulong>)list, val, (NetworkDelivery)2); } finally { ((IDisposable)(*(FastBufferWriter*)(&val))/*cast due to .constrained prefix*/).Dispose(); } } if (message == "aj.LastWords.Start") { Spirit.Begin(player, value); } else { Spirit.Finish(player, value); } } private unsafe static void Refuse(ulong client, Refusal why, float resting) { //IL_0027: Unknown result type (might be due to invalid IL or missing references) //IL_002d: Unknown result type (might be due to invalid IL or missing references) //IL_0039: 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) //IL_004c: Unknown result type (might be due to invalid IL or missing references) if (client == 0L) { SpiritView.Refused(why, resting); return; } CustomMessagingManager messages = Messages; if (messages == null) { return; } FastBufferWriter val = default(FastBufferWriter); ((FastBufferWriter)(ref val))..ctor(5, (Allocator)2, -1); try { byte b = (byte)why; ((FastBufferWriter)(ref val)).WriteValueSafe<byte>(ref b, default(ForPrimitives)); ((FastBufferWriter)(ref val)).WriteValueSafe<float>(ref resting, default(ForPrimitives)); messages.SendNamedMessage("aj.LastWords.Refused", client, val, (NetworkDelivery)2); } finally { ((IDisposable)(*(FastBufferWriter*)(&val))/*cast due to .constrained prefix*/).Dispose(); } } } [DefaultExecutionOrder(10000)] internal class SpiritRunner : MonoBehaviour { private void Update() { SpiritNet.HostTick(); Spirit.TickAll(); SpiritView.Tick(); } private void LateUpdate() { Spirit.LateTickAll(); SpiritView.LateTick(); } private void OnDestroy() { Spirit.Clear(); SpiritView.Reset(); SpiritNet.Unregister(); } } internal static class SpiritView { private enum Shown { Nothing, Speaking, Refused, NoBody, Resting, Ready } private const float ViewDistance = 2.5f; private const float ViewHeight = 0.2f; private const float RefusalSeconds = 2.5f; private const float LineGap = 30f; private const float LineScale = 0.8f; private static InputAction speakKey = null; private static TextMeshProUGUI? line; private static DeadBodyInfo? speakingFrom; private static float speakingUntil; private static float restingUntil; private static string refusal = ""; private static float refusalUntil; private static Shown shown; private static int shownCount = -1; internal static void CreateKey(string path) { //IL_000a: Unknown result type (might be due to invalid IL or missing references) //IL_0014: Expected O, but got Unknown speakKey = new InputAction("LastWordsSpeak", (InputActionType)1, path, (string)null, (string)null, (string)null); speakKey.Enable(); } internal static void Speaking(DeadBodyInfo body, float seconds) { speakingFrom = body; speakingUntil = Time.time + seconds; refusal = ""; } internal static void StoppedSpeaking() { speakingFrom = null; } internal static void Resting(float cooldown) { restingUntil = Time.time + cooldown; } internal static void Refused(SpiritNet.Refusal why, float resting) { object obj; switch (why) { case SpiritNet.Refusal.Resting: restingUntil = Time.time + resting; return; default: obj = ""; break; case SpiritNet.Refusal.NoBody: obj = "Your body is gone"; break; } refusal = (string)obj; refusalUntil = Time.time + 2.5f; } internal static void Reset() { speakingFrom = null; speakingUntil = (restingUntil = (refusalUntil = 0f)); refusal = ""; line = null; } internal static void Tick() { PlayerControllerB val = (((Object)(object)GameNetworkManager.Instance != (Object)null) ? GameNetworkManager.Instance.localPlayerController : null); if ((Object)(object)val == (Object)null || (Object)(object)HUDManager.Instance == (Object)null) { return; } EnsureLine(); if (!val.isPlayerDead) { Show(Shown.Nothing, 0); return; } bool flag = (Object)(object)Spirit.BodyOf(val) != (Object)null; if ((Object)(object)speakingFrom == (Object)null && Time.time >= restingUntil && flag && speakKey.WasPressedThisFrame() && !val.isTypingChat && !val.quickMenuManager.isMenuOpen) { SpiritNet.Ask((int)val.playerClientId); } if ((Object)(object)speakingFrom != (Object)null) { Show(Shown.Speaking, Mathf.CeilToInt(Mathf.Max(0f, speakingUntil - Time.time))); } else if (Time.time < refusalUntil && refusal != "") { Show(Shown.Refused, 0); } else if (!flag) { Show(Shown.NoBody, 0); } else if (Time.time < restingUntil) { Show(Shown.Resting, Mathf.CeilToInt(restingUntil - Time.time)); } else { Show(Shown.Ready, 0); } } private static void Show(Shown what, int count) { if (!((Object)(object)line == (Object)null) && (what != shown || count != shownCount)) { shown = what; shownCount = count; TextMeshProUGUI val = line; ((TMP_Text)val).text = what switch { Shown.Speaking => $"Your spirit speaks… {count}", Shown.Refused => refusal, Shown.NoBody => "Your body is gone", Shown.Resting => $"Your spirit rests… {count}", Shown.Ready => "[" + InputActionRebindingExtensions.GetBindingDisplayString(speakKey, (DisplayStringOptions)0, (string)null) + "] Speak from your body", _ => "", }; } } internal static void LateTick() { //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_005d: Unknown result type (might be due to invalid IL or missing references) //IL_0062: Unknown result type (might be due to invalid IL or missing references) //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) //IL_0086: Unknown result type (might be due to invalid IL or missing references) //IL_008e: 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_00cc: Unknown result type (might be due to invalid IL or missing references) //IL_00d1: Unknown result type (might be due to invalid IL or missing references) //IL_00d3: Unknown result type (might be due to invalid IL or missing references) //IL_00d8: Unknown result type (might be due to invalid IL or missing references) PlayerControllerB val = (((Object)(object)GameNetworkManager.Instance != (Object)null) ? GameNetworkManager.Instance.localPlayerController : null); if (!((Object)(object)speakingFrom == (Object)null) && !((Object)(object)val == (Object)null) && val.isPlayerDead) { Transform spectateCameraPivot = val.spectateCameraPivot; spectateCameraPivot.position = speakingFrom.bodyParts[5].position + Vector3.up * 0.2f; Transform transform = ((Component)StartOfRound.Instance.spectateCamera).transform; Vector3 val2 = -spectateCameraPivot.forward; float num = 2.5f; RaycastHit val3 = default(RaycastHit); if (Physics.SphereCast(spectateCameraPivot.position, 0.2f, val2, ref val3, 2.5f, StartOfRound.Instance.collidersAndRoomMaskAndDefault, (QueryTriggerInteraction)1)) { num = Mathf.Max(0.4f, ((RaycastHit)(ref val3)).distance - 0.1f); } transform.position = spectateCameraPivot.position + val2 * num; transform.LookAt(spectateCameraPivot); } } private static void EnsureLine() { //IL_007d: 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_008c: 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) if (!((Object)(object)line != (Object)null)) { TextMeshProUGUI spectatingPlayerText = HUDManager.Instance.spectatingPlayerText; if (!((Object)(object)spectatingPlayerText == (Object)null)) { line = Object.Instantiate<TextMeshProUGUI>(spectatingPlayerText, ((TMP_Text)spectatingPlayerText).transform.parent); ((Object)line).name = "LastWordsLine"; ((TMP_Text)line).text = ""; ((TMP_Text)line).fontSize = ((TMP_Text)spectatingPlayerText).fontSize * 0.8f; ((TMP_Text)line).rectTransform.anchoredPosition = ((TMP_Text)spectatingPlayerText).rectTransform.anchoredPosition + Vector2.down * 30f; shown = Shown.Nothing; shownCount = -1; } } } } internal class SpiritVoice { private readonly struct ChorusSettings { private readonly bool enabled; private readonly float dry; private readonly float wet1; private readonly float wet2; private readonly float wet3; private readonly float delay; private readonly float rate; private readonly float depth; private ChorusSettings(AudioChorusFilter c) { enabled = ((Behaviour)c).enabled; dry = c.dryMix; wet1 = c.wetMix1; wet2 = c.wetMix2; wet3 = c.wetMix3; delay = c.delay; rate = c.rate; depth = c.depth; } internal static ChorusSettings From(AudioChorusFilter c) { return new ChorusSettings(c); } internal void ApplyTo(AudioChorusFilter c) { c.dryMix = dry; c.wetMix1 = wet1; c.wetMix2 = wet2; c.wetMix3 = wet3; c.delay = delay; c.rate = rate; c.depth = depth; ((Behaviour)c).enabled = enabled; } } private readonly struct ReverbSettings { private readonly AudioReverbPreset preset; private readonly float dry; private readonly float room; private readonly float roomHF; private readonly float roomLF; private readonly float decay; private readonly float decayHFRatio; private readonly float reflections; private readonly float reflectionsDelay; private readonly float level; private readonly float delay; private readonly float hfReference; private readonly float lfReference; private readonly float diffusion; private readonly float density; private ReverbSettings(AudioReverbFilter r) { //IL_0002: Unknown result type (might be due to invalid IL or missing references) //IL_0007: Unknown result type (might be due to invalid IL or missing references) preset = r.reverbPreset; dry = r.dryLevel; room = r.room; roomHF = r.roomHF; roomLF = r.roomLF; decay = r.decayTime; decayHFRatio = r.decayHFRatio; reflections = r.reflectionsLevel; reflectionsDelay = r.reflectionsDelay; level = r.reverbLevel; delay = r.reverbDelay; hfReference = r.hfReference; lfReference = r.lfReference; diffusion = r.diffusion; density = r.density; } internal static ReverbSettings From(AudioReverbFilter r) { return new ReverbSettings(r); } internal void ApplyTo(AudioReverbFilter r) { //IL_0002: 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) //IL_0014: Invalid comparison between Unknown and I4 r.reverbPreset = preset; if ((int)preset == 27) { r.dryLevel = dry; r.room = room; r.roomHF = roomHF; r.roomLF = roomLF; r.decayTime = decay; r.decayHFRatio = decayHFRatio; r.reflectionsLevel = reflections; r.reflectionsDelay = reflectionsDelay; r.reverbLevel = level; r.reverbDelay = delay; r.hfReference = hfReference; r.lfReference = lfReference; r.diffusion = diffusion; r.density = density; } } } private const float HighPassHz = 380f; private const float ChorusDry = 0.7f; private const float ChorusWet = 0.35f; private const float ChorusDelayMs = 24f; private const float ChorusRate = 0.5f; private const float ChorusDepth = 0.06f; private const float ReverbDry = 0f; private const float ReverbRoom = -250f; private const float ReverbRoomHF = -150f; private const float ReverbDecay = 3.4f; private const float ReverbDecayHFRatio = 1.25f; private const float ReverbReflections = -900f; private const float ReverbReflectionsDelay = 0.02f; private const float ReverbLevel = 250f; private const float ReverbDelay = 0.045f; private const float Pitch = 1.06f; private const float FullVolumeFraction = 0.15f; private readonly PlayerControllerB player; private readonly DeadBodyInfo body; private readonly PlayerVoiceIngameSettings settings; private readonly AudioSource source; private readonly AudioReverbFilter? reverb; private readonly AudioHighPassFilter? highPass; private readonly AudioLowPassFilter? lowPass; private readonly AudioChorusFilter chorus; private readonly bool addedChorus; private readonly ChorusSettings savedChorus; private readonly AudioRolloffMode savedRolloff; private readonly float savedMinDistance; private readonly float savedMaxDistance; private readonly AnimationCurve savedCurve; private readonly bool savedBypassEffects; private readonly bool savedBypassListenerEffects; private readonly bool savedHighPassOn; private readonly bool savedLowPassOn; private readonly bool savedReverbOn; private readonly float savedHighPassHz; private readonly float savedHighPassQ; private readonly ReverbSettings savedReverb; internal bool IsCurrent { get { if ((Object)(object)source != (Object)null && (Object)(object)settings != (Object)null) { return (Object)(object)player.currentVoiceChatAudioSource == (Object)(object)source; } return false; } } private SpiritVoice(PlayerControllerB player, DeadBodyInfo body, PlayerVoiceIngameSettings settings, AudioSource source) { //IL_0091: Unknown result type (might be due to invalid IL or missing references) //IL_0096: Unknown result type (might be due to invalid IL or missing references) this.player = player; this.body = body; this.settings = settings; this.source = source; GameObject gameObject = ((Component)source).gameObject; reverb = gameObject.GetComponent<AudioReverbFilter>(); highPass = gameObject.GetComponent<AudioHighPassFilter>(); lowPass = gameObject.GetComponent<AudioLowPassFilter>(); addedChorus = (Object)(object)gameObject.GetComponent<AudioChorusFilter>() == (Object)null; chorus = (addedChorus ? gameObject.AddComponent<AudioChorusFilter>() : gameObject.GetComponent<AudioChorusFilter>()); savedChorus = ChorusSettings.From(chorus); savedRolloff = source.rolloffMode; savedMinDistance = source.minDistance; savedMaxDistance = source.maxDistance; savedCurve = source.GetCustomCurve((AudioSourceCurveType)0); savedBypassEffects = source.bypassEffects; savedBypassListenerEffects = source.bypassListenerEffects; savedHighPassOn = (Object)(object)highPass != (Object)null && ((Behaviour)highPass).enabled; savedHighPassHz = (((Object)(object)highPass != (Object)null) ? highPass.cutoffFrequency : 0f); savedHighPassQ = (((Object)(object)highPass != (Object)null) ? highPass.highpassResonanceQ : 1f); savedLowPassOn = (Object)(object)lowPass != (Object)null && ((Behaviour)lowPass).enabled; savedReverbOn = (Object)(object)reverb != (Object)null && ((Behaviour)reverb).enabled; savedReverb = (((Object)(object)reverb != (Object)null) ? ReverbSettings.From(reverb) : default(ReverbSettings)); } internal static float Loudness(PlayerControllerB p) { VoicePlayerState val = p.voicePlayerState; DissonanceComms val2 = (((Object)(object)StartOfRound.Instance != (Object)null) ? StartOfRound.Instance.voiceChatModule : null); if ((Object)(object)val2 != (Object)null && (Object)(object)p == (Object)(object)GameNetworkManager.Instance.localPlayerController) { val = val2.FindPlayer(val2.LocalPlayerName); } if (val == null || !val.IsSpeaking) { return 0f; } return val.Amplitude; } internal static SpiritVoice? Create(PlayerControllerB player, DeadBodyInfo body) { PlayerVoiceIngameSettings currentVoiceChatIngameSettings = player.currentVoiceChatIngameSettings; AudioSource currentVoiceChatAudioSource = player.currentVoiceChatAudioSource; if ((Object)(object)currentVoiceChatIngameSettings == (Object)null || (Object)(object)currentVoiceChatAudioSource == (Object)null || player.voicePlayerState == null) { return null; } return new SpiritVoice(player, body, currentVoiceChatIngameSettings, currentVoiceChatAudioSource); } internal void Apply() { //IL_00b5: Unknown result type (might be due to invalid IL or missing references) //IL_00ba: Unknown result type (might be due to invalid IL or missing references) //IL_00cb: Unknown result type (might be due to invalid IL or missing references) //IL_00d0: Unknown result type (might be due to invalid IL or missing references) //IL_00e1: Unknown result type (might be due to invalid IL or missing references) //IL_00e6: Unknown result type (might be due to invalid IL or missing references) //IL_00eb: Unknown result type (might be due to invalid IL or missing references) //IL_00f5: Expected O, but got Unknown if (IsCurrent && player.voicePlayerState != null) { PluginConfig boundConfig = Plugin.BoundConfig; player.voicePlayerState.Volume = Mathf.Clamp01(boundConfig.Volume.Value); settings.set2D = false; source.spatialBlend = 1f; source.bypassEffects = false; source.bypassListenerEffects = false; source.rolloffMode = (AudioRolloffMode)2; source.maxDistance = Mathf.Max(1f, boundConfig.HearingRange.Value); source.SetCustomCurve((AudioSourceCurveType)0, new AnimationCurve((Keyframe[])(object)new Keyframe[3] { new Keyframe(0f, 1f), new Keyframe(0.15f, 1f), new Keyframe(1f, 0f) })); if ((Object)(object)lowPass != (Object)null) { ((Behaviour)lowPass).enabled = true; } if ((Object)(object)highPass != (Object)null) { ((Behaviour)highPass).enabled = true; highPass.cutoffFrequency = 380f; highPass.highpassResonanceQ = 1f; } ((Behaviour)chorus).enabled = true; chorus.dryMix = 0.7f; chorus.wetMix1 = 0.35f; chorus.wetMix2 = 0.35f; chorus.wetMix3 = 0.35f; chorus.delay = 24f; chorus.rate = 0.5f; chorus.depth = 0.06f; if ((Object)(object)reverb != (Object)null) { ((Behaviour)reverb).enabled = true; reverb.reverbPreset = (AudioReverbPreset)27; reverb.dryLevel = 0f; reverb.room = -250f; reverb.roomHF = -150f; reverb.decayTime = 3.4f; reverb.decayHFRatio = 1.25f; reverb.reflectionsLevel = -900f; reverb.reflectionsDelay = 0.02f; reverb.reverbLevel = 250f; reverb.reverbDelay = 0.045f; } SetPitch(1.06f); } } internal void Place() { //IL_0034: Unknown result type (might be due to invalid IL or missing references) if ((Object)(object)body != (Object)null && (Object)(object)settings != (Object)null) { ((Component)settings).transform.position = body.bodyParts[0].position; } } internal void Restore() { //IL_0015: Unknown result type (might be due to invalid IL or missing references) if ((Object)(object)source != (Object)null) { source.rolloffMode = savedRolloff; source.minDistance = savedMinDistance; source.maxDistance = savedMaxDistance; source.SetCustomCurve((AudioSourceCurveType)0, savedCurve); source.bypassEffects = savedBypassEffects; source.bypassListenerEffects = savedBypassListenerEffects; } if ((Object)(object)highPass != (Object)null) { ((Behaviour)highPass).enabled = savedHighPassOn; highPass.cutoffFrequency = savedHighPassHz; highPass.highpassResonanceQ = savedHighPassQ; } if ((Object)(object)lowPass != (Object)null) { ((Behaviour)lowPass).enabled = savedLowPassOn; } if ((Object)(object)reverb != (Object)null) { savedReverb.ApplyTo(reverb); ((Behaviour)reverb).enabled = savedReverbOn; } if ((Object)(object)chorus != (Object)null) { if (addedChorus) { Object.Destroy((Object)(object)chorus); } else { savedChorus.ApplyTo(chorus); } } SetPitch(1f); StartOfRound.Instance.UpdatePlayerVoiceEffects(); } private void SetPitch(float pitch) { float[] array = (((Object)(object)SoundManager.Instance != (Object)null) ? SoundManager.Instance.playerVoicePitchTargets : null); if (array != null && player.playerClientId < (ulong)array.Length) { array[player.playerClientId] = pitch; } } } public static class PluginInfo { public const string PLUGIN_GUID = "aj.LastWords"; public const string PLUGIN_NAME = "LastWords"; public const string PLUGIN_VERSION = "1.0.1"; } } namespace LastWords.Configuration { public class PluginConfig { public ConfigEntry<string> SpeakKey; public ConfigEntry<float> MinSeconds; public ConfigEntry<float> MaxSeconds; public ConfigEntry<float> CooldownSeconds; public ConfigEntry<float> Volume; public ConfigEntry<float> HearingRange; public ConfigEntry<bool> TestKeys; public PluginConfig(ConfigFile cfg) { SpeakKey = cfg.Bind<string>("General", "Speak key", "<Keyboard>/r", "The key a dead player presses to go back to their body and speak (an Input System path, e.g. <Keyboard>/r or <Keyboard>/v)."); MinSeconds = cfg.Bind<float>("Speech", "Shortest speech", 5f, "Each time, the spirit can speak for a random time between the shortest and longest (seconds). The host's setting is used."); MaxSeconds = cfg.Bind<float>("Speech", "Longest speech", 10f, "See \"Shortest speech\"."); CooldownSeconds = cfg.Bind<float>("Speech", "Cooldown", 30f, "Seconds the spirit rests after speaking before it can speak again. The host's setting is used."); Volume = cfg.Bind<float>("Voice", "Spirit volume", 0.6f, "How loud the spirit is for the living, compared with a living player's voice (1 = as loud). Each player's own setting."); HearingRange = cfg.Bind<float>("Voice", "Hearing range", 15f, "How far (metres) from the body the spirit can be heard; it fades out to nothing at this distance. Each player's own setting."); TestKeys = cfg.Bind<bool>("Testing", "Test keys", false, "For testing only. When on, anyone can press F6 to drop dead (to have a body to speak from). Takes effect after restarting the game."); ClearUnusedEntries(cfg); } private void ClearUnusedEntries(ConfigFile cfg) { if (((object)cfg).GetType().GetProperty("OrphanedEntries", BindingFlags.Instance | BindingFlags.NonPublic)?.GetValue(cfg, null) is Dictionary<ConfigDefinition, string> dictionary) { dictionary.Clear(); } cfg.Save(); } } }