Decompiled source of Final Kill Replay v0.1.16

plugins/FinalKillReplay/FinalKillReplay.dll

Decompiled 3 hours ago
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Text.RegularExpressions;
using BepInEx;
using BepInEx.Configuration;
using FishNet.Object;
using HarmonyLib;
using TMPro;
using UnityEngine;
using UnityEngine.Events;
using UnityEngine.InputSystem;
using UnityEngine.InputSystem.Controls;
using UnityEngine.Rendering;
using UnityEngine.SceneManagement;

[assembly: CompilationRelaxations(8)]
[assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)]
[assembly: AssemblyVersion("0.0.0.0")]
namespace StraftatFinalKill;

[BepInPlugin("local.straftat.finalkillreplay", "Final Kill Replay", "0.1.16")]
public sealed class Plugin : BaseUnityPlugin
{
	private sealed class DeathEvent
	{
		public PlayerHealth Victim;

		public int VictimId;

		public int KillerId;

		public float Time;

		public bool Explosive;

		public bool Environment;
	}

	private sealed class Frame
	{
		public RenderTexture Image;

		public float Time;

		public float Health;

		public float MaxHealth;

		public AudioStamp Audio;
	}

	private sealed class Track
	{
		public readonly PlayerHealth Player;

		public readonly Camera Source;

		public readonly Transform View;

		public readonly Frame[] Frames;

		public readonly List<float> Shots = new List<float>();

		public readonly List<AudioClip> ShotClips = new List<AudioClip>();

		public int Next;

		public int Count;

		public readonly string Name;

		public readonly uint Generation;

		private int cachedPlayerId;

		private readonly Dictionary<Renderer, Renderer> bodies = new Dictionary<Renderer, Renderer>();

		private readonly Dictionary<Renderer, Mesh> bakedBodies = new Dictionary<Renderer, Mesh>();

		public int PlayerId
		{
			get
			{
				ClientInstance val = Client(Player);
				if (Object.op_Implicit((Object)(object)val))
				{
					cachedPlayerId = Field<int>(val, "PlayerId");
				}
				return cachedPlayerId;
			}
		}

		public Track(PlayerHealth p, Camera c, Transform v, int n)
		{
			Player = p;
			Source = c;
			View = v;
			Frames = new Frame[n];
			Name = PlayerName(p);
			Generation = Field<uint>(p, "spawnGeneration");
			ClientInstance val = Client(p);
			cachedPlayerId = (Object.op_Implicit((Object)(object)val) ? Field<int>(val, "PlayerId") : (-1));
		}

		public void ShowBody(List<Renderer> hidden, List<bool> states)
		{
			//IL_01ba: Unknown result type (might be due to invalid IL or missing references)
			//IL_01c6: Unknown result type (might be due to invalid IL or missing references)
			//IL_01de: 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_013f: Expected O, but got Unknown
			//IL_0162: Unknown result type (might be due to invalid IL or missing references)
			//IL_0169: Expected O, but got Unknown
			GameObject val = Field<GameObject>(Player, "graphics");
			if (!Object.op_Implicit((Object)(object)val))
			{
				return;
			}
			PlayerSetup component = ((Component)Player).GetComponent<PlayerSetup>();
			GameObject[] source = (GameObject[])(((object)Field<GameObject[]>(component, "gameObjectsToDisableForMe")) ?? ((object)new GameObject[0]));
			GameObject[] source2 = (GameObject[])(((object)Field<GameObject[]>(component, "meshesToChange")) ?? ((object)new GameObject[0]));
			Renderer[] componentsInChildren = val.GetComponentsInChildren<Renderer>(true);
			foreach (Renderer val2 in componentsInChildren)
			{
				bool flag = val2.enabled || (val2 is SkinnedMeshRenderer && source2.Contains(((Component)val2).gameObject));
				Transform val3 = ((Component)val2).transform;
				while (Object.op_Implicit((Object)(object)val3) && (Object)(object)val3 != (Object)(object)((Component)Player).transform)
				{
					if (!((Component)val3).gameObject.activeSelf && (Object)(object)((Component)val3).gameObject != (Object)(object)val && !source.Contains(((Component)val3).gameObject))
					{
						flag = false;
					}
					val3 = val3.parent;
				}
				if (!flag)
				{
					continue;
				}
				if (!bodies.TryGetValue(val2, out var value))
				{
					SkinnedMeshRenderer val4 = (SkinnedMeshRenderer)(object)((val2 is SkinnedMeshRenderer) ? val2 : null);
					MeshFilter component2 = ((Component)val2).GetComponent<MeshFilter>();
					if (!Object.op_Implicit((Object)(object)val4) && !Object.op_Implicit((Object)(object)component2))
					{
						continue;
					}
					GameObject val5 = new GameObject("FinalKillReplay.Body");
					MeshFilter val6 = val5.AddComponent<MeshFilter>();
					value = (Renderer)(object)val5.AddComponent<MeshRenderer>();
					value.enabled = false;
					if (Object.op_Implicit((Object)(object)val4))
					{
						Mesh val7 = new Mesh();
						((Object)val7).name = "FinalKillReplay.BodyPose";
						bakedBodies.Add(val2, val7);
						val6.sharedMesh = val7;
					}
					else
					{
						val6.sharedMesh = component2.sharedMesh;
					}
					bodies.Add(val2, value);
				}
				((Component)value).transform.SetPositionAndRotation(((Component)val2).transform.position, ((Component)val2).transform.rotation);
				((Component)value).transform.localScale = ((Component)val2).transform.lossyScale;
				value.sharedMaterials = val2.sharedMaterials;
				value.shadowCastingMode = (ShadowCastingMode)0;
				SkinnedMeshRenderer val8 = (SkinnedMeshRenderer)(object)((val2 is SkinnedMeshRenderer) ? val2 : null);
				if (Object.op_Implicit((Object)(object)val8) && Object.op_Implicit((Object)(object)val8.sharedMesh))
				{
					val8.BakeMesh(bakedBodies[val2]);
					bakedBodies[val2].RecalculateBounds();
				}
				hidden.Add(val2);
				states.Add(val2.forceRenderingOff);
				val2.forceRenderingOff = true;
				value.enabled = true;
			}
		}

		public void HideBody()
		{
			foreach (Renderer value in bodies.Values)
			{
				if (Object.op_Implicit((Object)(object)value))
				{
					value.enabled = false;
				}
			}
		}

		public Frame At(int i)
		{
			return Frames[(Next - Count + i + Frames.Length) % Frames.Length];
		}

		public void Dispose()
		{
			foreach (Renderer value in bodies.Values)
			{
				if (Object.op_Implicit((Object)(object)value))
				{
					Object.Destroy((Object)(object)((Component)value).gameObject);
				}
			}
			bodies.Clear();
			foreach (Mesh value2 in bakedBodies.Values)
			{
				if (Object.op_Implicit((Object)(object)value2))
				{
					Object.Destroy((Object)(object)value2);
				}
			}
			bakedBodies.Clear();
			Frame[] frames = Frames;
			foreach (Frame frame in frames)
			{
				if (frame != null && Object.op_Implicit((Object)(object)frame.Image))
				{
					frame.Image.Release();
					Object.Destroy((Object)(object)frame.Image);
				}
			}
		}
	}

	public const string Id = "local.straftat.finalkillreplay";

	private const float BufferSeconds = 5f;

	private const float HoldSeconds = 9f;

	internal static Plugin Instance;

	private Harmony patches;

	private ConfigEntry<bool> active;

	private ConfigEntry<bool> recordAudio;

	private ConfigEntry<int> audioDelayMs;

	private double playbackDspStart;

	private readonly AudioHistory audioHistory = new AudioHistory();

	private readonly List<ReplayAudioTap> audioTaps = new List<ReplayAudioTap>();

	private float nextAudioScan;

	private float clipStart;

	private ConfigEntry<int> width;

	private ConfigEntry<int> fps;

	private ConfigEntry<float> speed;

	private ConfigEntry<float> lead;

	private readonly Dictionary<int, Track> tracks = new Dictionary<int, Track>();

	private readonly List<DeathEvent> deaths = new List<DeathEvent>();

	private Camera recorder;

	private RenderTexture renderTarget;

	private AudioSource replayAudio;

	private AudioClip finalShotClip;

	private bool shotPlayed;

	private float nextScan;

	private float nextFrame;

	private float playbackStart;

	private float cursor;

	private float clipEnd;

	private float shotTime;

	private bool playing;

	private bool takeFinished;

	private bool preview;

	private string notice;

	private float noticeUntil;

	private float soloLoadRequested;

	private bool inputLogged;

	private bool legacyInputAvailable = true;

	private readonly bool[] keyHeld = new bool[3];

	private ReplayDriver driver;

	private Track replay;

	private string killerName;

	private GUIStyle title;

	private GUIStyle caption;

	private GUIStyle noticeBox;

	private Font gameFont;

	private float nextFontScan;

	private bool fontWaitLogged;

	internal static float Now => Time.realtimeSinceStartup;

	internal bool Holding
	{
		get
		{
			if (active.Value && playing && !preview)
			{
				return Now - playbackStart < 8.5f;
			}
			return false;
		}
	}

	private void Awake()
	{
		//IL_0070: Unknown result type (might be due to invalid IL or missing references)
		//IL_007a: Expected O, but got Unknown
		//IL_00af: Unknown result type (might be due to invalid IL or missing references)
		//IL_00b9: Expected O, but got Unknown
		//IL_00e5: Unknown result type (might be due to invalid IL or missing references)
		//IL_00ef: Expected O, but got Unknown
		//IL_0124: Unknown result type (might be due to invalid IL or missing references)
		//IL_012e: Expected O, but got Unknown
		//IL_0163: Unknown result type (might be due to invalid IL or missing references)
		//IL_016d: Expected O, but got Unknown
		//IL_0186: Unknown result type (might be due to invalid IL or missing references)
		//IL_0190: Expected O, but got Unknown
		//IL_0361: Unknown result type (might be due to invalid IL or missing references)
		//IL_036b: Expected O, but got Unknown
		//IL_0318: Unknown result type (might be due to invalid IL or missing references)
		//IL_0326: Expected O, but got Unknown
		Instance = this;
		active = ((BaseUnityPlugin)this).Config.Bind<bool>("General", "Enabled", true, "Enable recording and final-kill replay. Restart to apply changes.");
		recordAudio = ((BaseUnityPlugin)this).Config.Bind<bool>("Recording", "RecordAudio", true, "Replay the locally heard game audio, including the lead-up. Restart required.");
		audioDelayMs = ((BaseUnityPlugin)this).Config.Bind<int>("Replay", "AudioDelayMilliseconds", 0, new ConfigDescription("Optional extra playback delay after frame/audio alignment. Positive values play sound later. Restart required.", (AcceptableValueBase)(object)new AcceptableValueRange<int>(0, 500), new object[0]));
		width = ((BaseUnityPlugin)this).Config.Bind<int>("Recording", "Width", 768, new ConfigDescription("16:9 recording width. 768 gives 432p; 960 gives 540p. Restart required.", (AcceptableValueBase)(object)new AcceptableValueRange<int>(320, 960), new object[0]));
		fps = ((BaseUnityPlugin)this).Config.Bind<int>("Recording", "FramesPerSecond", 40, new ConfigDescription("Recording rate. Four players at 768/40 use about 1013 MiB of GPU memory. Restart required.", (AcceptableValueBase)(object)new AcceptableValueRange<int>(15, 60), new object[0]));
		speed = ((BaseUnityPlugin)this).Config.Bind<float>("Replay", "SlowMotionSpeed", 0.2f, new ConfigDescription("Playback speed immediately before the finishing shot.", (AcceptableValueBase)(object)new AcceptableValueRange<float>(0.1f, 0.5f), new object[0]));
		lead = ((BaseUnityPlugin)this).Config.Bind<float>("Replay", "SlowMotionLeadSeconds", 0.45f, new ConfigDescription("Begin slowing this many seconds before the recorded shot.", (AcceptableValueBase)(object)new AcceptableValueRange<float>(0.15f, 0.8f), new object[0]));
		if (!active.Value)
		{
			return;
		}
		try
		{
			patches = new Harmony("local.straftat.finalkillreplay");
			Patch(typeof(GameManager), "RpcLogic___AchievementTakeResult_451821701", null, "TakeResult");
			Patch(typeof(GameManager), "WaitForDraw", null, "WrapTake");
			Patch(typeof(GameManager), "RpcLogic___ObserversRoundSpawn_2166136261", "NewTake", null);
			Patch(typeof(PlayerHealth), "RpcLogic___DisablePlayerObjectForAll_2166136261", "Died", null);
			Patch(typeof(PlayerHealth), "RpcLogic___ExplodeForAll_576886416", "Exploded", null);
			Patch(typeof(PlayerHealth), "RpcLogic___DespawnObjectObservers_2166136261", "Died", null);
			Patch(typeof(FirstPersonController), "RpcLogic___ExplodeOnDeath_2166136261", "EnvironmentDeath", null);
			Patch(typeof(Weapon), "OnShoot", "Shot", null);
			Patch(typeof(PauseManager), "Update", null, "EnsureRuntime");
			foreach (Type item in from t in typeof(Weapon).Assembly.GetTypes()
				where typeof(Weapon).IsAssignableFrom(t)
				select t)
			{
				MethodInfo[] methods = item.GetMethods(BindingFlags.DeclaredOnly | BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
				foreach (MethodInfo methodInfo in methods)
				{
					if (methodInfo.Name.StartsWith("RpcLogic___ShootObserversEffect_") || methodInfo.Name.StartsWith("RpcLogic___SoundObserversEffect_"))
					{
						patches.Patch((MethodBase)methodInfo, new HarmonyMethod(typeof(Hooks), "Shot", (Type[])null), (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null);
					}
				}
			}
			SceneManager.sceneLoaded += SceneLoaded;
			Application.onBeforeRender += new UnityAction(EnsureRuntime);
			((BaseUnityPlugin)this).Logger.LogInfo((object)"Final Kill Replay 0.1.16 loaded. Waiting for menu runtime.");
		}
		catch (Exception ex)
		{
			if (patches != null)
			{
				patches.UnpatchSelf();
			}
			active.Value = false;
			((BaseUnityPlugin)this).Logger.LogError((object)("Replay disabled: this game version is incompatible. " + ex));
		}
	}

	internal void EnsureRuntime()
	{
		//IL_0071: Unknown result type (might be due to invalid IL or missing references)
		//IL_0077: Expected O, but got Unknown
		if (active != null && active.Value && !Object.op_Implicit((Object)(object)driver))
		{
			((BaseUnityPlugin)this).Logger.LogInfo((object)("Creating replay runtime after menu update. Plugin enabled=" + ((Behaviour)this).enabled + ", active=" + ((Component)this).gameObject.activeInHierarchy));
			GameObject val = new GameObject("FinalKillReplay.Runtime");
			Object.DontDestroyOnLoad((Object)(object)val);
			driver = val.AddComponent<ReplayDriver>();
			driver.Owner = this;
		}
	}

	private void EnsureRecorder()
	{
		//IL_0013: Unknown result type (might be due to invalid IL or missing references)
		//IL_0019: Expected O, but got Unknown
		if (!Object.op_Implicit((Object)(object)recorder))
		{
			GameObject val = new GameObject("FinalKillReplay.Recorder");
			Object.DontDestroyOnLoad((Object)(object)val);
			recorder = val.AddComponent<Camera>();
			((Behaviour)recorder).enabled = false;
			replayAudio = val.AddComponent<AudioSource>();
			replayAudio.playOnAwake = false;
			replayAudio.spatialBlend = 0f;
		}
	}

	private void Patch(Type type, string method, string prefix, string postfix)
	{
		//IL_0036: 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)
		MethodInfo methodInfo = AccessTools.Method(type, method, (Type[])null, (Type[])null);
		if (methodInfo == null)
		{
			throw new MissingMethodException(type.FullName, method);
		}
		patches.Patch((MethodBase)methodInfo, (prefix == null) ? ((HarmonyMethod)null) : new HarmonyMethod(typeof(Hooks), prefix, (Type[])null), (postfix == null) ? ((HarmonyMethod)null) : new HarmonyMethod(typeof(Hooks), postfix, (Type[])null), (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null);
	}

	internal static T Field<T>(object target, string name)
	{
		if (target == null)
		{
			return default(T);
		}
		FieldInfo fieldInfo = AccessTools.Field(target.GetType(), name);
		if (!(fieldInfo == null))
		{
			return (T)fieldInfo.GetValue(target);
		}
		return default(T);
	}

	private static ClientInstance Client(PlayerHealth p)
	{
		if (!Object.op_Implicit((Object)(object)p))
		{
			return null;
		}
		PlayerValues component = ((Component)p).GetComponent<PlayerValues>();
		return Field<ClientInstance>(component, "playerClient");
	}

	private static string PlayerName(PlayerHealth p)
	{
		ClientInstance val = Client(p);
		return ReplayDisplay.PlainName(Object.op_Implicit((Object)(object)val) ? Field<string>(val, "PlayerName") : "FINAL KILL");
	}

	internal void TickInput()
	{
		//IL_0065: Unknown result type (might be due to invalid IL or missing references)
		//IL_006a: Unknown result type (might be due to invalid IL or missing references)
		if (active == null || !active.Value)
		{
			return;
		}
		FindGameFont();
		if (recordAudio.Value && Now >= nextAudioScan)
		{
			nextAudioScan = Now + 0.25f;
			AudioListener[] array = Resources.FindObjectsOfTypeAll<AudioListener>();
			foreach (AudioListener val in array)
			{
				Scene scene = ((Component)val).gameObject.scene;
				if (((Scene)(ref scene)).IsValid())
				{
					ReplayAudioTap replayAudioTap = ((Component)val).GetComponent<ReplayAudioTap>();
					if (!Object.op_Implicit((Object)(object)replayAudioTap))
					{
						replayAudioTap = ((Component)val).gameObject.AddComponent<ReplayAudioTap>();
						audioTaps.Add(replayAudioTap);
					}
					replayAudioTap.Listener = val;
					replayAudioTap.SampleRate = AudioSettings.outputSampleRate;
					replayAudioTap.History = audioHistory;
				}
			}
		}
		bool flag = false;
		foreach (ReplayAudioTap audioTap in audioTaps)
		{
			if (Object.op_Implicit((Object)(object)audioTap))
			{
				flag |= (audioTap.CaptureEnabled = !flag && Object.op_Implicit((Object)(object)audioTap.Listener) && ((Behaviour)audioTap.Listener).isActiveAndEnabled);
			}
		}
		if (!inputLogged)
		{
			inputLogged = true;
			((BaseUnityPlugin)this).Logger.LogInfo((object)("Replay input loop active. Keyboard=" + (Keyboard.current != null) + "; recorder=" + Object.op_Implicit((Object)(object)recorder)));
		}
		CheckShortcut(0, (KeyCode)288, Keyboard.current != null && ((ButtonControl)Keyboard.current.f7Key).isPressed);
		CheckShortcut(1, (KeyCode)289, Keyboard.current != null && ((ButtonControl)Keyboard.current.f8Key).isPressed);
		CheckShortcut(2, (KeyCode)290, Keyboard.current != null && ((ButtonControl)Keyboard.current.f9Key).isPressed);
	}

	private void CheckShortcut(int index, KeyCode key, bool held)
	{
		//IL_0008: Unknown result type (might be due to invalid IL or missing references)
		//IL_0041: Unknown result type (might be due to invalid IL or missing references)
		if (legacyInputAvailable)
		{
			try
			{
				held = Input.GetKey(key) || held;
			}
			catch (InvalidOperationException)
			{
				legacyInputAvailable = false;
			}
		}
		bool flag = held && !keyHeld[index];
		keyHeld[index] = held;
		if (flag)
		{
			RunShortcut(key);
		}
	}

	private void RunShortcut(KeyCode key)
	{
		//IL_000b: Unknown result type (might be due to invalid IL or missing references)
		//IL_001b: Unknown result type (might be due to invalid IL or missing references)
		//IL_0021: Invalid comparison between Unknown and I4
		//IL_0031: Unknown result type (might be due to invalid IL or missing references)
		//IL_0037: Invalid comparison between Unknown and I4
		//IL_0047: Unknown result type (might be due to invalid IL or missing references)
		//IL_004d: Invalid comparison between Unknown and I4
		((BaseUnityPlugin)this).Logger.LogInfo((object)("Replay shortcut received: " + key));
		try
		{
			if ((int)key == 288 && !playing)
			{
				OpenSoloTestMap();
			}
			if ((int)key == 289 && playing)
			{
				StopReplay();
			}
			if ((int)key == 290 && !playing)
			{
				PreviewLatestShot();
			}
		}
		catch (Exception ex)
		{
			Notify("Replay command failed; details are in BepInEx/LogOutput.log.");
			((BaseUnityPlugin)this).Logger.LogError((object)ex);
		}
	}

	internal void TickRecording()
	{
		if (!active.Value)
		{
			return;
		}
		try
		{
			EnsureRecorder();
			if (playing)
			{
				if (Now - playbackStart > 8.5f)
				{
					StopReplay();
					return;
				}
				cursor = clipStart + Timeline.Advance(0f, (float)(AudioSettings.dspTime - playbackDspStart), shotTime - clipStart, lead.Value, speed.Value);
				if (!shotPlayed && cursor >= shotTime)
				{
					shotPlayed = true;
					if (Object.op_Implicit((Object)(object)finalShotClip))
					{
						replayAudio.pitch = Mathf.Max(0.3f, speed.Value);
						replayAudio.PlayOneShot(finalShotClip);
					}
				}
				float num = clipStart + Timeline.Advance(0f, (float)(AudioSettings.dspTime - playbackDspStart) - (float)audioDelayMs.Value / 1000f, shotTime - clipStart, lead.Value, speed.Value);
				if (cursor >= clipEnd && num >= clipEnd)
				{
					StopReplay();
				}
			}
			else
			{
				if (takeFinished)
				{
					return;
				}
				if (Now >= nextScan)
				{
					Scan();
					nextScan = Now + 0.5f;
				}
				if (Now < nextFrame)
				{
					return;
				}
				nextFrame = Now + 1f / (float)fps.Value;
				{
					foreach (Track t in tracks.Values)
					{
						if (Object.op_Implicit((Object)(object)t.Player))
						{
							if (Field<bool>(t.Player, "isKilled"))
							{
								RecordDeath(t.Player);
							}
							DeathEvent deathEvent = deaths.FirstOrDefault((DeathEvent d) => d.VictimId == ((Object)t.Player).GetInstanceID());
							if (!((deathEvent != null) ? (Now > deathEvent.Time + 0.6f) : (!((Component)t.Player).gameObject.activeInHierarchy)))
							{
								Capture(t);
							}
						}
					}
					return;
				}
			}
		}
		catch (Exception ex)
		{
			((BaseUnityPlugin)this).Logger.LogError((object)("Recording stopped safely: " + ex));
			takeFinished = true;
			StopReplay();
		}
	}

	private void Notify(string message)
	{
		notice = message;
		noticeUntil = Now + 4f;
		((BaseUnityPlugin)this).Logger.LogInfo((object)message);
	}

	private void OpenSoloTestMap()
	{
		SceneMotor instance = SceneMotor.Instance;
		LobbyController instance2 = LobbyController.Instance;
		PauseManager instance3 = PauseManager.Instance;
		if (!Object.op_Implicit((Object)(object)instance) || !Object.op_Implicit((Object)(object)instance2) || !((NetworkBehaviour)instance).IsServer || !((NetworkBehaviour)instance).IsClient || !Object.op_Implicit((Object)(object)instance2.LocalPlayerController))
		{
			Notify("F7 solo test: host a lobby first, without inviting anyone.");
			return;
		}
		if (ClientInstance.playerInstances.Count != 1 || !Object.op_Implicit((Object)(object)SteamLobby.Instance) || SteamLobby.Instance.players.Count != 1)
		{
			Notify("F7 solo test: you must be the only player in the lobby.");
			return;
		}
		if (!Object.op_Implicit((Object)(object)instance3) || !Field<bool>(instance3, "inMainMenu") || Object.FindObjectsOfType<PlayerHealth>().Length != 0)
		{
			Notify("F7 solo test: return to the lobby first. In a map, use F9 to preview a shot.");
			return;
		}
		if (Field<bool>(instance, "inLoadingScreen") || Now < soloLoadRequested)
		{
			Notify("F7 solo test: a map is already loading.");
			return;
		}
		List<string> list = Field<List<string>>(instance, "PlayListMaps");
		if (list == null || list.Count == 0 || string.IsNullOrEmpty(list[0]))
		{
			Notify("F7 solo test: select at least one map in your lobby first.");
			return;
		}
		soloLoadRequested = Now + 15f;
		Notify("Loading solo test map. Once inside, fire a shot and press F9.");
		instance.EnterScene(list[0], ((NetworkBehaviour)instance2.LocalPlayerController).Owner);
	}

	private void PreviewLatestShot()
	{
		PlayerHealth[] array = Object.FindObjectsOfType<PlayerHealth>();
		if (array.Length != 1)
		{
			Notify("F9 preview: host a lobby alone, select a map, then press F7 to enter it.");
			return;
		}
		PlayerHealth val = array[0];
		bool flag = Object.op_Implicit((Object)(object)SceneMotor.Instance) && Field<bool>(SceneMotor.Instance, "testMap");
		if (!((NetworkBehaviour)val).IsOwner && !flag)
		{
			Notify("F9 preview: no local player camera available.");
			return;
		}
		if (takeFinished)
		{
			Notify("F9 preview: wait for the next take.");
			return;
		}
		if (!tracks.TryGetValue(((Object)val).GetInstanceID(), out var t) || t.Count < 3)
		{
			Notify("F9 preview: move around for a few seconds, then fire a shot.");
			return;
		}
		int num = t.Shots.FindLastIndex((float x) => x >= t.At(0).Time && x <= t.At(t.Count - 1).Time);
		if (num < 0)
		{
			Notify("F9 preview: fire a shot, wait a moment, then press F9 within 4 seconds.");
			return;
		}
		float num2 = t.Shots[num];
		if (t.At(t.Count - 1).Time < num2 + 0.15f)
		{
			Notify("F9 preview: wait a moment after the shot, then press F9 again.");
			return;
		}
		if (!BeginReplay(t, num2, num2 + 0.6f, isPreview: true))
		{
			Notify("F9 preview: not enough recorded footage yet.");
			return;
		}
		noticeUntil = 0f;
		((BaseUnityPlugin)this).Logger.LogInfo((object)("Solo replay preview started: " + t.Count + " frames. No take result or host delay was triggered."));
	}

	private bool BeginReplay(Track t, float shot, float end, bool isPreview)
	{
		float start = Mathf.Max(t.At(0).Time, shot - 2.2f);
		float num = Mathf.Min(t.At(t.Count - 1).Time, end);
		if (num <= start)
		{
			return false;
		}
		replay = t;
		shotTime = shot;
		cursor = start;
		clipEnd = num;
		int num2 = t.Shots.IndexOf(shot);
		finalShotClip = ((num2 >= 0) ? t.ShotClips[num2] : null);
		shotPlayed = false;
		preview = isPreview;
		killerName = t.Name;
		playbackStart = Now;
		playbackDspStart = AudioSettings.dspTime;
		clipStart = start;
		playing = true;
		Frame[] source = (from i in Enumerable.Range(0, t.Count)
			select t.At(i) into f
			where f.Audio.Generation == audioHistory.Stamp.Generation
			select f).ToArray();
		if (recordAudio.Value && audioHistory.BeginMapped(source.Select((Frame f) => f.Time - start).ToArray(), source.Select((Frame f) => f.Audio).ToArray(), num - start, playbackDspStart, shot - start, lead.Value, speed.Value, (float)audioDelayMs.Value / 1000f))
		{
			finalShotClip = null;
			((BaseUnityPlugin)this).Logger.LogInfo((object)"Replay includes recorded local game audio.");
		}
		else if (recordAudio.Value)
		{
			((BaseUnityPlugin)this).Logger.LogWarning((object)"No captured audio for this replay; using shot sound fallback.");
		}
		return true;
	}

	private void Scan()
	{
		uint generation = (Object.op_Implicit((Object)(object)GameManager.Instance) ? Field<uint>(GameManager.Instance, "currentTakeGeneration") : 0u);
		int[] array = (from x in tracks
			where (Object.op_Implicit((Object)(object)GameManager.Instance) && x.Value.Generation != generation) || (!Object.op_Implicit((Object)(object)x.Value.Player) && x.Value.Count == 0)
			select x.Key).ToArray();
		foreach (int key in array)
		{
			tracks[key].Dispose();
			tracks.Remove(key);
		}
		PlayerHealth[] array2 = Object.FindObjectsOfType<PlayerHealth>();
		foreach (PlayerHealth val in array2)
		{
			if (Object.op_Implicit((Object)(object)GameManager.Instance) && Field<uint>(val, "spawnGeneration") != generation)
			{
				continue;
			}
			int instanceID = ((Object)val).GetInstanceID();
			if (tracks.ContainsKey(instanceID) || tracks.Count >= 4)
			{
				continue;
			}
			Transform val2 = Field<Transform>(val, "playerCamera");
			if (Object.op_Implicit((Object)(object)val2))
			{
				Camera val3 = ((Component)val2).GetComponent<Camera>();
				if (!Object.op_Implicit((Object)(object)val3))
				{
					val3 = ((Component)val).GetComponentsInChildren<Camera>(true).FirstOrDefault();
				}
				if (Object.op_Implicit((Object)(object)val3))
				{
					tracks.Add(instanceID, new Track(val, val3, val2, Mathf.CeilToInt(5f * (float)fps.Value)));
				}
			}
		}
	}

	private void Capture(Track t)
	{
		//IL_00fb: Unknown result type (might be due to invalid IL or missing references)
		//IL_0105: Expected O, but got Unknown
		//IL_01fe: Unknown result type (might be due to invalid IL or missing references)
		//IL_022f: Unknown result type (might be due to invalid IL or missing references)
		//IL_0240: Unknown result type (might be due to invalid IL or missing references)
		//IL_018e: Unknown result type (might be due to invalid IL or missing references)
		//IL_0198: Expected O, but got Unknown
		if (!Object.op_Implicit((Object)(object)t.View) || !Object.op_Implicit((Object)(object)t.Source))
		{
			return;
		}
		float now = Now;
		AudioStamp stamp = audioHistory.Stamp;
		float num = Field<float>(t.Player, "health");
		float maxHealth = Field<float>(t.Player, "fullHealth");
		if (Field<bool>(t.Player, "isKilled") || deaths.Any((DeathEvent d) => d.VictimId == ((Object)t.Player).GetInstanceID()))
		{
			num = 0f;
		}
		int num2 = width.Value * 9 / 16;
		Frame frame = t.Frames[t.Next];
		if (frame == null)
		{
			frame = new Frame();
			frame.Image = new RenderTexture(width.Value, num2, 0, (RenderTextureFormat)0);
			((Object)frame.Image).name = "FinalKillReplay.Frame";
			frame.Image.useMipMap = false;
			frame.Image.antiAliasing = 1;
			if (!frame.Image.Create())
			{
				Object.Destroy((Object)(object)frame.Image);
				throw new InvalidOperationException("Could not allocate replay frame.");
			}
			t.Frames[t.Next] = frame;
		}
		if (!Object.op_Implicit((Object)(object)renderTarget))
		{
			renderTarget = new RenderTexture(width.Value, num2, 24, (RenderTextureFormat)0);
			if (!renderTarget.Create())
			{
				throw new InvalidOperationException("Could not allocate recording surface.");
			}
		}
		recorder.CopyFrom(t.Source);
		((Behaviour)recorder).enabled = false;
		recorder.targetTexture = renderTarget;
		recorder.rect = new Rect(0f, 0f, 1f, 1f);
		recorder.aspect = 1.7777778f;
		((Component)recorder).transform.SetPositionAndRotation(t.View.position, t.View.rotation);
		if (recorder.cullingMask == 0 && Object.op_Implicit((Object)(object)Camera.main))
		{
			recorder.cullingMask = Camera.main.cullingMask;
		}
		GameObject val = Field<GameObject>(t.Player, "graphics");
		Renderer[] array = (Renderer[])(Object.op_Implicit((Object)(object)val) ? ((Array)val.GetComponentsInChildren<Renderer>(true)) : ((Array)new Renderer[0]));
		bool[] array2 = new bool[array.Length];
		RenderTexture val2 = RenderTexture.active;
		List<Renderer> list = new List<Renderer>();
		List<bool> list2 = new List<bool>();
		try
		{
			for (int num3 = 0; num3 < array.Length; num3++)
			{
				array2[num3] = array[num3].forceRenderingOff;
				array[num3].forceRenderingOff = true;
			}
			foreach (Track body in tracks.Values)
			{
				if (body != t && Object.op_Implicit((Object)(object)body.Player) && ((NetworkBehaviour)body.Player).IsOwner && ((Component)body.Player).gameObject.activeInHierarchy && !Field<bool>(body.Player, "isKilled") && !deaths.Any((DeathEvent d) => d.VictimId == ((Object)body.Player).GetInstanceID()))
				{
					body.ShowBody(list, list2);
				}
			}
			Camera obj = recorder;
			obj.cullingMask |= 1;
			recorder.Render();
			Graphics.Blit((Texture)(object)renderTarget, frame.Image);
			frame.Time = now;
			frame.Audio = stamp;
			frame.Health = Mathf.Max(0f, num);
			frame.MaxHealth = maxHealth;
			t.Next = (t.Next + 1) % t.Frames.Length;
			t.Count = Math.Min(t.Count + 1, t.Frames.Length);
		}
		finally
		{
			foreach (Track value in tracks.Values)
			{
				value.HideBody();
			}
			for (int num4 = 0; num4 < list.Count; num4++)
			{
				if (Object.op_Implicit((Object)(object)list[num4]))
				{
					list[num4].forceRenderingOff = list2[num4];
				}
			}
			for (int num5 = 0; num5 < array.Length; num5++)
			{
				if (Object.op_Implicit((Object)(object)array[num5]))
				{
					array[num5].forceRenderingOff = array2[num5];
				}
			}
			recorder.targetTexture = null;
			RenderTexture.active = val2;
		}
	}

	internal void RecordShot(Weapon weapon)
	{
		if (takeFinished || !Object.op_Implicit((Object)(object)weapon))
		{
			return;
		}
		GameObject val = Field<GameObject>(weapon, "rootObject");
		if (!Object.op_Implicit((Object)(object)val))
		{
			val = Field<GameObject>(weapon, "lastPlayerHolder");
		}
		PlayerHealth val2 = (Object.op_Implicit((Object)(object)val) ? val.GetComponentInParent<PlayerHealth>() : null);
		if (!Object.op_Implicit((Object)(object)val2))
		{
			val2 = ((Component)weapon).GetComponentInParent<PlayerHealth>();
		}
		FirstPersonController val3 = Field<FirstPersonController>(weapon, "playerController");
		if (!Object.op_Implicit((Object)(object)val2) && Object.op_Implicit((Object)(object)val3))
		{
			val2 = ((Component)val3).GetComponent<PlayerHealth>();
		}
		if (Object.op_Implicit((Object)(object)val2) && tracks.TryGetValue(((Object)val2).GetInstanceID(), out var value))
		{
			value.Shots.Add(Now);
			value.ShotClips.Add(Field<AudioClip>(weapon, "fireClip"));
			while (value.Shots.Count > 0 && value.Shots[0] < Now - 5f)
			{
				value.Shots.RemoveAt(0);
				value.ShotClips.RemoveAt(0);
			}
		}
	}

	internal void RecordDeath(PlayerHealth p, bool explosive = false, bool environment = false)
	{
		if (!takeFinished && Object.op_Implicit((Object)(object)p) && (!Object.op_Implicit((Object)(object)GameManager.Instance) || Field<uint>(p, "spawnGeneration") == Field<uint>(GameManager.Instance, "currentTakeGeneration")))
		{
			DeathEvent deathEvent = deaths.FirstOrDefault((DeathEvent d) => d.VictimId == ((Object)p).GetInstanceID());
			if (deathEvent != null)
			{
				deathEvent.Explosive |= explosive;
				deathEvent.Environment |= environment;
				return;
			}
			Transform val = Field<Transform>(p, "killer");
			PlayerHealth val2 = (Object.op_Implicit((Object)(object)val) ? ((Component)val).GetComponentInParent<PlayerHealth>() : null);
			deaths.Add(new DeathEvent
			{
				Victim = p,
				VictimId = ((Object)p).GetInstanceID(),
				KillerId = (Object.op_Implicit((Object)(object)val2) ? ((Object)val2).GetInstanceID() : 0),
				Time = Now,
				Explosive = explosive,
				Environment = environment
			});
		}
	}

	internal void Result(int winningTeam, int[] finalVictims)
	{
		if (takeFinished)
		{
			return;
		}
		foreach (Track value2 in tracks.Values)
		{
			if (Object.op_Implicit((Object)(object)value2.Player) && Field<bool>(value2.Player, "isKilled"))
			{
				RecordDeath(value2.Player);
			}
		}
		StopReplay();
		takeFinished = true;
		foreach (DeathEvent d in deaths.OrderByDescending((DeathEvent deathEvent) => deathEvent.Time))
		{
			if (Now - d.Time > 2.5f || !tracks.TryGetValue(d.VictimId, out var victimTrack) || (finalVictims != null && finalVictims.Length > 0 && !finalVictims.Contains(victimTrack.PlayerId)))
			{
				continue;
			}
			Transform val = (Object.op_Implicit((Object)(object)d.Victim) ? Field<Transform>(d.Victim, "killer") : null);
			PlayerHealth val2 = (Object.op_Implicit((Object)(object)val) ? ((Component)val).GetComponentInParent<PlayerHealth>() : null);
			tracks.TryGetValue(Object.op_Implicit((Object)(object)val2) ? ((Object)val2).GetInstanceID() : d.KillerId, out var value);
			bool flag = d.Explosive || d.Environment || winningTeam < 0;
			if (flag)
			{
				value = victimTrack;
			}
			else if (!IsWinningTrack(value, winningTeam) || value == victimTrack || value.Count < 3)
			{
				value = (from x in tracks.Values
					where IsWinningTrack(x, winningTeam) && x != victimTrack && x.Count >= 3
					orderby (Object.op_Implicit((Object)(object)x.Player) && Field<bool>(x.Player, "isKilled")) ? 1 : 0, x.PlayerId
					select x).FirstOrDefault();
			}
			if (value == null || value.Count < 3)
			{
				continue;
			}
			float shot = ((flag || d.Explosive) ? Mathf.Min(d.Time, value.At(value.Count - 1).Time) : value.Shots.Where((float x) => x <= d.Time + 0.08f && x >= d.Time - 3f).DefaultIfEmpty(d.Time).Max());
			if (BeginReplay(value, shot, d.Time + 0.6f, isPreview: false))
			{
				if (flag || d.Explosive)
				{
					finalShotClip = null;
				}
				if (flag)
				{
					killerName = value.Name + " / VICTIM VIEW";
				}
				((BaseUnityPlugin)this).Logger.LogInfo((object)("Final kill replay: " + killerName + ", " + value.Count + " frames; environment=" + d.Environment + ", victimView=" + flag));
				return;
			}
		}
		((BaseUnityPlugin)this).Logger.LogWarning((object)("Replay skipped: winnerTeam=" + winningTeam + ", deaths=" + deaths.Count + ", generation=" + (Object.op_Implicit((Object)(object)GameManager.Instance) ? Field<uint>(GameManager.Instance, "currentTakeGeneration").ToString() : "unknown") + "; tracks=" + string.Join("; ", tracks.Values.Select((Track t) => t.Name + " [player=" + t.PlayerId + ", generation=" + t.Generation + ", frames=" + t.Count + ", active=" + (Object.op_Implicit((Object)(object)t.Player) && ((Component)t.Player).gameObject.activeInHierarchy) + "]").ToArray()) + "; deathDetails=" + string.Join("; ", deaths.Select((DeathEvent deathEvent) => "victim=" + deathEvent.VictimId + ", age=" + (Now - deathEvent.Time).ToString("F2") + ", explosive=" + deathEvent.Explosive + ", environment=" + deathEvent.Environment).ToArray())));
	}

	private static bool IsWinningTrack(Track t, int winningTeam)
	{
		if (t != null && t.PlayerId >= 0 && Object.op_Implicit((Object)(object)ScoreManager.Instance))
		{
			return ScoreManager.Instance.GetTeamId(t.PlayerId) == winningTeam;
		}
		return false;
	}

	private void StopReplay()
	{
		if (playing)
		{
			audioHistory.Stop();
		}
		playing = false;
		replay = null;
		finalShotClip = null;
		if (Object.op_Implicit((Object)(object)replayAudio))
		{
			replayAudio.Stop();
		}
		if (preview)
		{
			foreach (Track value in tracks.Values)
			{
				value.Count = 0;
				value.Next = 0;
				value.Shots.Clear();
				value.ShotClips.Clear();
			}
			nextFrame = 0f;
		}
		preview = false;
	}

	internal void ResetTake()
	{
		audioHistory.Reset();
		StopReplay();
		takeFinished = false;
		deaths.Clear();
		foreach (Track value in tracks.Values)
		{
			value.Dispose();
		}
		tracks.Clear();
		nextScan = 0f;
		nextFrame = 0f;
		((BaseUnityPlugin)this).Logger.LogInfo((object)"Replay reset for next take: retired all previous player recordings.");
	}

	private void SceneLoaded(Scene scene, LoadSceneMode mode)
	{
		((BaseUnityPlugin)this).Logger.LogInfo((object)("Replay scene callback: " + ((Scene)(ref scene)).name));
		Clear();
		EnsureRuntime();
	}

	private void Clear()
	{
		audioHistory.Reset();
		nextAudioScan = 0f;
		StopReplay();
		takeFinished = false;
		deaths.Clear();
		noticeUntil = 0f;
		foreach (Track value in tracks.Values)
		{
			value.Dispose();
		}
		tracks.Clear();
		nextScan = 0f;
		if (Object.op_Implicit((Object)(object)renderTarget))
		{
			renderTarget.Release();
			Object.Destroy((Object)(object)renderTarget);
			renderTarget = null;
		}
	}

	private void OnDestroy()
	{
		//IL_0017: Unknown result type (might be due to invalid IL or missing references)
		//IL_0021: Expected O, but got Unknown
		((BaseUnityPlugin)this).Logger.LogWarning((object)"Replay plugin component destroyed.");
		Application.onBeforeRender -= new UnityAction(EnsureRuntime);
		SceneManager.sceneLoaded -= SceneLoaded;
		if (patches != null)
		{
			patches.UnpatchSelf();
		}
		Clear();
		if (Object.op_Implicit((Object)(object)recorder))
		{
			Object.Destroy((Object)(object)((Component)recorder).gameObject);
		}
		foreach (ReplayAudioTap audioTap in audioTaps)
		{
			if (Object.op_Implicit((Object)(object)audioTap))
			{
				audioTap.History = null;
				Object.Destroy((Object)(object)audioTap);
			}
		}
		audioTaps.Clear();
		if (Object.op_Implicit((Object)(object)driver))
		{
			Object.Destroy((Object)(object)((Component)driver).gameObject);
		}
		if ((Object)(object)Instance == (Object)(object)this)
		{
			Instance = null;
		}
	}

	private void FindGameFont()
	{
		if (Object.op_Implicit((Object)(object)gameFont) || Now < nextFontScan)
		{
			return;
		}
		nextFontScan = Now + 1f;
		gameFont = ((IEnumerable<Font>)Resources.FindObjectsOfTypeAll<Font>()).FirstOrDefault((Func<Font, bool>)((Font f) => Object.op_Implicit((Object)(object)f) && ((Object)f).name.Replace(" ", "").Replace("-", "").IndexOf("centurygothic", StringComparison.OrdinalIgnoreCase) >= 0));
		if (!Object.op_Implicit((Object)(object)gameFont))
		{
			TMP_FontAsset[] array = Resources.FindObjectsOfTypeAll<TMP_FontAsset>();
			foreach (TMP_FontAsset val in array)
			{
				if (Object.op_Implicit((Object)(object)val) && ((Object)val).name.Replace(" ", "").IndexOf("centurygothic", StringComparison.OrdinalIgnoreCase) >= 0 && Object.op_Implicit((Object)(object)val.sourceFontFile))
				{
					gameFont = val.sourceFontFile;
					break;
				}
			}
		}
		if (Object.op_Implicit((Object)(object)gameFont))
		{
			title = (caption = (noticeBox = null));
			((BaseUnityPlugin)this).Logger.LogInfo((object)("Replay UI font: " + ((Object)gameFont).name));
		}
		else if (!fontWaitLogged)
		{
			fontWaitLogged = true;
			((BaseUnityPlugin)this).Logger.LogInfo((object)"Waiting for the game's Century Gothic font to load.");
		}
	}

	private GUIStyle TextStyle(GUIStyle original)
	{
		//IL_0001: Unknown result type (might be due to invalid IL or missing references)
		//IL_0007: Expected O, but got Unknown
		GUIStyle val = new GUIStyle(original);
		if (Object.op_Implicit((Object)(object)gameFont))
		{
			val.font = gameFont;
		}
		val.fontStyle = (FontStyle)0;
		val.richText = false;
		return val;
	}

	internal void DrawOverlay()
	{
		//IL_00a4: Unknown result type (might be due to invalid IL or missing references)
		//IL_00cf: Unknown result type (might be due to invalid IL or missing references)
		//IL_00d5: Invalid comparison between Unknown and I4
		//IL_014a: Unknown result type (might be due to invalid IL or missing references)
		//IL_014f: Unknown result type (might be due to invalid IL or missing references)
		//IL_0150: Unknown result type (might be due to invalid IL or missing references)
		//IL_0170: Unknown result type (might be due to invalid IL or missing references)
		//IL_017f: Unknown result type (might be due to invalid IL or missing references)
		//IL_0117: Unknown result type (might be due to invalid IL or missing references)
		//IL_0128: Unknown result type (might be due to invalid IL or missing references)
		//IL_0132: Expected O, but got Unknown
		//IL_023d: Unknown result type (might be due to invalid IL or missing references)
		//IL_0287: Unknown result type (might be due to invalid IL or missing references)
		//IL_02a3: Unknown result type (might be due to invalid IL or missing references)
		//IL_02c1: Unknown result type (might be due to invalid IL or missing references)
		//IL_02d0: Unknown result type (might be due to invalid IL or missing references)
		//IL_0324: Unknown result type (might be due to invalid IL or missing references)
		//IL_034e: Unknown result type (might be due to invalid IL or missing references)
		//IL_041c: Unknown result type (might be due to invalid IL or missing references)
		//IL_0426: Unknown result type (might be due to invalid IL or missing references)
		//IL_047c: Unknown result type (might be due to invalid IL or missing references)
		//IL_0486: Unknown result type (might be due to invalid IL or missing references)
		//IL_04df: Unknown result type (might be due to invalid IL or missing references)
		//IL_0508: Unknown result type (might be due to invalid IL or missing references)
		//IL_0517: Unknown result type (might be due to invalid IL or missing references)
		//IL_0521: Unknown result type (might be due to invalid IL or missing references)
		//IL_04c9: Unknown result type (might be due to invalid IL or missing references)
		//IL_04b3: Unknown result type (might be due to invalid IL or missing references)
		//IL_057e: Unknown result type (might be due to invalid IL or missing references)
		//IL_05b1: Unknown result type (might be due to invalid IL or missing references)
		//IL_05e0: Unknown result type (might be due to invalid IL or missing references)
		//IL_05ef: Unknown result type (might be due to invalid IL or missing references)
		if (active == null || !active.Value)
		{
			return;
		}
		if (noticeBox == null)
		{
			noticeBox = TextStyle(GUI.skin.box);
			noticeBox.fontSize = 16;
			noticeBox.wordWrap = true;
			noticeBox.alignment = (TextAnchor)4;
		}
		GUI.depth = -10000;
		if (!playing && Now < noticeUntil)
		{
			GUI.depth = -10000;
			GUI.Box(new Rect((float)Screen.width * 0.15f, 24f, (float)Screen.width * 0.7f, 48f), notice, noticeBox);
		}
		if (!playing || replay == null || (int)Event.current.type != 7)
		{
			return;
		}
		GUI.depth = -10000;
		if (title == null)
		{
			title = TextStyle(GUI.skin.label);
			title.alignment = (TextAnchor)4;
			title.normal.textColor = Color.white;
			caption = new GUIStyle(title);
			caption.fontStyle = (FontStyle)0;
			caption.richText = false;
		}
		Color color = GUI.color;
		GUI.color = Color.black;
		GUI.DrawTexture(new Rect(0f, 0f, (float)Screen.width, (float)Screen.height), (Texture)(object)Texture2D.whiteTexture);
		GUI.color = Color.white;
		float num = Mathf.Min((float)Screen.width, (float)Screen.height * 16f / 9f);
		float num2 = num * 9f / 16f;
		Rect val = default(Rect);
		((Rect)(ref val))..ctor(((float)Screen.width - num) / 2f, ((float)Screen.height - num2) / 2f, num, num2);
		Frame frame = replay.At(0);
		for (int i = 1; i < replay.Count; i++)
		{
			Frame frame2 = replay.At(i);
			if (Mathf.Abs(frame2.Time - cursor) >= Mathf.Abs(frame.Time - cursor))
			{
				break;
			}
			frame = frame2;
		}
		GUI.DrawTexture(val, (Texture)(object)frame.Image, (ScaleMode)0, false);
		float num3 = (float)Screen.height * 0.11f;
		float num4 = (float)Screen.height * 0.16f;
		float num5 = (float)Screen.height - num4;
		GUI.color = new Color(0f, 0f, 0f, 0.85f);
		GUI.DrawTexture(new Rect(0f, 0f, (float)Screen.width, num3), (Texture)(object)Texture2D.whiteTexture);
		GUI.DrawTexture(new Rect(0f, num5, (float)Screen.width, num4), (Texture)(object)Texture2D.whiteTexture);
		GUI.color = Color.white;
		title.fontSize = Mathf.RoundToInt((float)Screen.height * 0.038f);
		caption.fontSize = Mathf.RoundToInt((float)Screen.height * 0.021f);
		GUI.Label(new Rect(0f, 0f, (float)Screen.width, num3), "FINAL KILL", title);
		GUI.Label(new Rect(0f, num5, (float)Screen.width, num4 * 0.3f), killerName, caption);
		float num6 = Mathf.Min((float)Screen.width * 0.45f, (float)Screen.height * 0.48f);
		Rect val2 = default(Rect);
		((Rect)(ref val2))..ctor(((float)Screen.width - num6) / 2f, num5 + num4 * 0.35f, num6, num4 * 0.27f);
		bool flag = frame.MaxHealth > 0f && !float.IsNaN(frame.Health) && !float.IsInfinity(frame.Health) && !float.IsInfinity(frame.MaxHealth);
		float num7 = (flag ? Mathf.Clamp01(frame.Health / frame.MaxHealth) : 0f);
		GUI.color = new Color(0.75f, 0.78f, 0.7f);
		GUI.DrawTexture(val2, (Texture)(object)Texture2D.whiteTexture);
		Rect val3 = default(Rect);
		((Rect)(ref val3))..ctor(((Rect)(ref val2)).x + 2f, ((Rect)(ref val2)).y + 2f, ((Rect)(ref val2)).width - 4f, ((Rect)(ref val2)).height - 4f);
		GUI.color = new Color(0.06f, 0.07f, 0.06f);
		GUI.DrawTexture(val3, (Texture)(object)Texture2D.whiteTexture);
		GUI.color = ((num7 <= 0.25f) ? new Color(0.6f, 0.12f, 0.1f) : ((num7 <= 0.5f) ? new Color(0.5f, 0.36f, 0.06f) : new Color(0.16f, 0.4f, 0.18f)));
		GUI.DrawTexture(new Rect(((Rect)(ref val3)).x, ((Rect)(ref val3)).y, ((Rect)(ref val3)).width * num7, ((Rect)(ref val3)).height), (Texture)(object)Texture2D.whiteTexture);
		GUI.color = Color.white;
		GUI.Label(val2, flag ? ("HEALTH  " + ReplayDisplay.HealthPercent(frame.Health, frame.MaxHealth) + " / 100") : "HEALTH  --", caption);
		GUI.Label(new Rect(0f, num5 + num4 * 0.7f, (float)Screen.width, num4 * 0.25f), preview ? "SOLO PREVIEW  /  F8 TO SKIP  /  GAME CONTINUES" : "REPLAY  /  F8 TO SKIP", caption);
		GUI.color = new Color(0.8f, 0.85f, 0.55f);
		GUI.DrawTexture(new Rect((float)Screen.width * 0.04f, num3 - 2f, (float)Screen.width * 0.92f, 2f), (Texture)(object)Texture2D.whiteTexture);
		GUI.color = color;
	}
}
public sealed class ReplayAudioTap : MonoBehaviour
{
	public volatile AudioHistory History;

	public volatile int SampleRate;

	public volatile bool CaptureEnabled;

	public AudioListener Listener;

	private void OnAudioFilterRead(float[] data, int channels)
	{
		AudioHistory history = History;
		if (history != null && CaptureEnabled)
		{
			history.Process(data, channels, SampleRate, AudioSettings.dspTime);
		}
	}
}
public sealed class ReplayDriver : MonoBehaviour
{
	internal Plugin Owner;

	private void Update()
	{
		if (Object.op_Implicit((Object)(object)Owner))
		{
			Owner.TickInput();
		}
	}

	private void LateUpdate()
	{
		if (Object.op_Implicit((Object)(object)Owner))
		{
			Owner.TickRecording();
		}
	}

	private void OnGUI()
	{
		if (Object.op_Implicit((Object)(object)Owner))
		{
			Owner.DrawOverlay();
		}
	}
}
internal static class Hooks
{
	private static readonly FieldInfo waitDuration = AccessTools.Field(typeof(WaitForSeconds), "m_Seconds");

	public static void EnsureRuntime()
	{
		Guard(delegate
		{
			Plugin.Instance.EnsureRuntime();
		});
	}

	public static void Shot(Weapon __instance)
	{
		Guard(delegate
		{
			Plugin.Instance.RecordShot(__instance);
		});
	}

	public static void Died(PlayerHealth __instance)
	{
		Guard(delegate
		{
			Plugin.Instance.RecordDeath(__instance);
		});
	}

	public static void Exploded(PlayerHealth __instance, bool __0)
	{
		Guard(delegate
		{
			Plugin.Instance.RecordDeath(__instance, __0);
		});
	}

	public static void EnvironmentDeath(FirstPersonController __instance)
	{
		Guard(delegate
		{
			Plugin.Instance.RecordDeath(((Component)__instance).GetComponent<PlayerHealth>(), explosive: false, environment: true);
		});
	}

	public static void TakeResult(int __0, int[] __1)
	{
		Guard(delegate
		{
			Plugin.Instance.Result(__0, __1);
		});
	}

	public static void NewTake()
	{
		Guard(delegate
		{
			Plugin.Instance.ResetTake();
		});
	}

	public static void WrapTake(ref IEnumerator __result)
	{
		__result = ReplayTransition.Wrap(__result, () => Object.op_Implicit((Object)(object)Plugin.Instance) && Plugin.Instance.Holding, () => Time.time, (object value) => (!(value is WaitForSeconds) || !(waitDuration != null)) ? ((float?)null) : ((float?)waitDuration.GetValue(value)), (float seconds) => (object)new WaitForSeconds(seconds));
	}

	private static void Guard(Action action)
	{
		if (!Object.op_Implicit((Object)(object)Plugin.Instance))
		{
			return;
		}
		try
		{
			action();
		}
		catch (Exception ex)
		{
			Debug.LogError((object)("FinalKillReplay hook: " + ex));
		}
	}
}
public static class Timeline
{
	public static float Advance(float cursor, float realDelta, float shot, float lead, float speed)
	{
		float num = shot - lead;
		float num2 = shot + 0.18f;
		float num3 = Math.Max(0f, realDelta);
		if (cursor < num)
		{
			float num4 = Math.Min(num - cursor, num3);
			cursor += num4;
			num3 -= num4;
		}
		if (cursor < num2 && num3 > 0f)
		{
			float num5 = Math.Min(num2 - cursor, num3 * speed);
			cursor += num5;
			num3 -= num5 / speed;
		}
		return cursor + num3 * 0.55f;
	}
}
public struct AudioStamp
{
	public long Sample;

	public int Generation;
}
public sealed class AudioHistory
{
	private readonly object gate = new object();

	private float[] ring;

	private int rate;

	private int channels;

	private int next;

	private int count;

	private double endTime;

	private double sourceStart;

	private double playStart;

	private float shot;

	private float lead;

	private float speed;

	private float duration;

	private float delay;

	private bool playing;

	private long total;

	private int generation = 1;

	private float[] frameTimes;

	private double[] frameSamples;

	public AudioStamp Stamp
	{
		get
		{
			lock (gate)
			{
				return new AudioStamp
				{
					Sample = total,
					Generation = ((count > 0) ? generation : (-1))
				};
			}
		}
	}

	public void Reset()
	{
		lock (gate)
		{
			playing = false;
			count = (next = 0);
			total = 0L;
			generation++;
			frameTimes = null;
			frameSamples = null;
		}
	}

	public void Stop()
	{
		Reset();
	}

	public bool BeginMapped(float[] times, AudioStamp[] stamps, float length, double now, float shotOffset, float slowLead, float slowSpeed, float audioDelay)
	{
		lock (gate)
		{
			if (times == null || stamps == null || times.Length != stamps.Length || times.Length < 2 || count == 0 || length <= 0f)
			{
				return false;
			}
			for (int i = 0; i < times.Length; i++)
			{
				if (stamps[i].Generation != generation || stamps[i].Sample > total || (i > 0 && (times[i] <= times[i - 1] || stamps[i].Sample < stamps[i - 1].Sample)))
				{
					return false;
				}
			}
			if (stamps[^1].Sample <= total - count)
			{
				return false;
			}
			frameTimes = (float[])times.Clone();
			frameSamples = new double[stamps.Length];
			for (int j = 0; j < stamps.Length; j++)
			{
				frameSamples[j] = stamps[j].Sample;
			}
			duration = length;
			playStart = now;
			shot = shotOffset;
			lead = slowLead;
			speed = slowSpeed;
			delay = audioDelay;
			playing = true;
			return true;
		}
	}

	private double SampleAt(float position)
	{
		if (position < frameTimes[0] || position > frameTimes[frameTimes.Length - 1])
		{
			return -1.0;
		}
		int num = 0;
		int num2 = frameTimes.Length - 1;
		while (num2 - num > 1)
		{
			int num3 = (num + num2) / 2;
			if (frameTimes[num3] <= position)
			{
				num = num3;
			}
			else
			{
				num2 = num3;
			}
		}
		double num4 = (position - frameTimes[num]) / (frameTimes[num2] - frameTimes[num]);
		return frameSamples[num] + (frameSamples[num2] - frameSamples[num]) * num4;
	}

	public bool Begin(double start, double finish, double now, float shotOffset, float slowLead, float slowSpeed, float audioDelay = 0f)
	{
		lock (gate)
		{
			if (count == 0 || finish <= start || endTime < start || endTime - (double)count / (double)rate > finish)
			{
				return false;
			}
			sourceStart = start;
			playStart = now;
			duration = (float)(finish - start);
			frameTimes = null;
			frameSamples = null;
			shot = shotOffset;
			lead = slowLead;
			speed = slowSpeed;
			delay = audioDelay;
			playing = true;
			return true;
		}
	}

	public void Process(float[] data, int outputChannels, int sampleRate, double dspTime)
	{
		lock (gate)
		{
			if (outputChannels < 1 || sampleRate < 1)
			{
				return;
			}
			int num = data.Length / outputChannels;
			if (rate != sampleRate || channels != outputChannels || ring == null)
			{
				rate = sampleRate;
				channels = outputChannels;
				ring = new float[rate * 6 * channels];
				next = (count = 0);
				total = 0L;
				generation++;
				playing = false;
			}
			int num2 = ring.Length / channels;
			if (playing)
			{
				for (int i = 0; i < num; i++)
				{
					float num3 = (float)(dspTime - playStart + (double)i / (double)rate) - delay;
					float num4 = Timeline.Advance(0f, Math.Max(0f, num3), shot, lead, speed);
					double num5 = ((frameTimes == null) ? ((sourceStart + (double)num4 - endTime) * (double)rate + (double)count) : (SampleAt(num4) - (double)total + (double)count));
					int num6 = (int)Math.Floor(num5);
					float num7 = (float)(num5 - (double)num6);
					for (int j = 0; j < channels; j++)
					{
						float num8 = 0f;
						if (num3 >= 0f && num4 < duration && num6 >= 0 && num6 + 1 < count)
						{
							int num9 = (next - count + num6 + num2) % num2 * channels + j;
							int num10 = (next - count + num6 + 1 + num2) % num2 * channels + j;
							num8 = ring[num9] + (ring[num10] - ring[num9]) * num7;
							num8 *= Math.Min(1f, Math.Min(num4, duration - num4) / 0.01f);
						}
						data[i * channels + j] = num8;
					}
				}
			}
			else
			{
				for (int k = 0; k < num; k++)
				{
					Array.Copy(data, k * channels, ring, next * channels, channels);
					next = (next + 1) % num2;
					count = Math.Min(count + 1, num2);
					total++;
				}
				endTime = dspTime + (double)num / (double)rate;
			}
		}
	}
}
public static class ReplayTransition
{
	public static IEnumerator Wrap(IEnumerator original, Func<bool> holding, Func<float> clock, Func<object, float?> waitSeconds, Func<float, object> makeWait)
	{
		float credit = 0f;
		try
		{
			while (original.MoveNext())
			{
				float began = clock();
				bool held = false;
				while (holding())
				{
					held = true;
					yield return null;
				}
				if (held)
				{
					credit += Math.Max(0f, clock() - began);
				}
				object current = original.Current;
				float? duration = waitSeconds(current);
				if (duration.HasValue && credit > 0f)
				{
					float num = Math.Min(Math.Max(0f, duration.Value), credit);
					credit -= num;
					float num2 = duration.Value - num;
					if (num2 <= 0f)
					{
						continue;
					}
					current = makeWait(num2);
				}
				yield return current;
			}
		}
		finally
		{
			if (original is IDisposable disposable)
			{
				disposable.Dispose();
			}
		}
	}
}
public static class ReplayDisplay
{
	public static string PlainName(string value)
	{
		string text = Regex.Replace(value ?? "", "<[^>]*>", "").Replace('\r', ' ').Replace('\n', ' ')
			.Trim();
		if (text.Length != 0)
		{
			return text;
		}
		return "PLAYER";
	}

	public static int HealthPercent(float current, float maximum)
	{
		if (maximum <= 0f || float.IsNaN(maximum) || float.IsInfinity(maximum) || float.IsNaN(current) || float.IsInfinity(current))
		{
			return 0;
		}
		return (int)Math.Ceiling(Math.Max(0f, Math.Min(1f, current / maximum)) * 100f);
	}
}