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);
}
}