Decompiled source of HeadcamReplay v1.1.3

HeadcamReplay.dll

Decompiled a day ago
using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Runtime.Versioning;
using System.Threading.Tasks;
using BepInEx;
using BepInEx.Configuration;
using BepInEx.Logging;
using HarmonyLib;
using Photon.Pun;
using UnityEngine;

[assembly: CompilationRelaxations(8)]
[assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)]
[assembly: Debuggable(DebuggableAttribute.DebuggingModes.Default | DebuggableAttribute.DebuggingModes.DisableOptimizations | DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints | DebuggableAttribute.DebuggingModes.EnableEditAndContinue)]
[assembly: AssemblyTitle("RepoStarterMod")]
[assembly: AssemblyDescription("")]
[assembly: AssemblyConfiguration("")]
[assembly: AssemblyCompany("")]
[assembly: AssemblyProduct("RepoStarterMod")]
[assembly: AssemblyCopyright("Copyright ©  2025")]
[assembly: AssemblyTrademark("")]
[assembly: ComVisible(false)]
[assembly: Guid("97d352f5-4c2d-4aec-a09b-d82de3937eea")]
[assembly: AssemblyFileVersion("1.0.0.0")]
[assembly: TargetFramework(".NETFramework,Version=v4.8", FrameworkDisplayName = ".NET Framework 4.8")]
[assembly: AssemblyVersion("1.0.0.0")]
[BepInPlugin("com.yourname.headcamreplay", "Headcam Replay", "1.1.3")]
public sealed class HeadcamReplay : BaseUnityPlugin
{
	[CompilerGenerated]
	private sealed class <>c__DisplayClass34_0
	{
		public Texture2D screenshot;

		public HeadcamReplay <>4__this;

		internal void <CaptureCurrentScreenFrame>b__0()
		{
			byte[] item = ImageConversion.EncodeToPNG(screenshot);
			Object.Destroy((Object)(object)screenshot);
			int num = Mathf.RoundToInt(<>4__this.cfgReplaySeconds.Value * (float)<>4__this.cfgBufferFPS.Value);
			if (<>4__this.frameBuffer.Count >= num)
			{
				<>4__this.frameBuffer.Dequeue();
			}
			<>4__this.frameBuffer.Enqueue(item);
		}
	}

	[CompilerGenerated]
	private sealed class <>c__DisplayClass39_0
	{
		public string execPath;

		public HeadcamReplay <>4__this;
	}

	[CompilerGenerated]
	private sealed class <>c__DisplayClass39_1
	{
		public string args;

		public bool finished;

		public <>c__DisplayClass39_0 CS$<>8__locals1;

		internal void <FinalizeExportCoroutine>b__0()
		{
			try
			{
				ProcessStartInfo startInfo = new ProcessStartInfo
				{
					FileName = CS$<>8__locals1.execPath,
					Arguments = args,
					UseShellExecute = false,
					CreateNoWindow = true
				};
				Process process = new Process
				{
					StartInfo = startInfo
				};
				process.Start();
				process.WaitForExit();
				finished = true;
			}
			catch (Exception ex)
			{
				CS$<>8__locals1.<>4__this.PublicLogger.LogError((object)("Failed to run FFmpeg: ApplicationName='" + CS$<>8__locals1.execPath + "', Arguments='" + args + "', Error= " + ex.Message));
				finished = true;
			}
		}
	}

	[CompilerGenerated]
	private sealed class <CaptureCurrentScreenFrame>d__34 : IEnumerator<object>, IDisposable, IEnumerator
	{
		private int <>1__state;

		private object <>2__current;

		public HeadcamReplay <>4__this;

		private <>c__DisplayClass34_0 <>8__1;

		object IEnumerator<object>.Current
		{
			[DebuggerHidden]
			get
			{
				return <>2__current;
			}
		}

		object IEnumerator.Current
		{
			[DebuggerHidden]
			get
			{
				return <>2__current;
			}
		}

		[DebuggerHidden]
		public <CaptureCurrentScreenFrame>d__34(int <>1__state)
		{
			this.<>1__state = <>1__state;
		}

		[DebuggerHidden]
		void IDisposable.Dispose()
		{
			<>8__1 = null;
			<>1__state = -2;
		}

		private bool MoveNext()
		{
			//IL_0068: Unknown result type (might be due to invalid IL or missing references)
			//IL_0072: Expected O, but got Unknown
			switch (<>1__state)
			{
			default:
				return false;
			case 0:
				<>1__state = -1;
				<>8__1 = new <>c__DisplayClass34_0();
				<>8__1.<>4__this = <>4__this;
				if (<>4__this.isCapturingFrame)
				{
					return false;
				}
				<>4__this.isCapturingFrame = true;
				<>2__current = (object)new WaitForEndOfFrame();
				<>1__state = 1;
				return true;
			case 1:
				<>1__state = -1;
				<>8__1.screenshot = ScreenCapture.CaptureScreenshotAsTexture();
				<>2__current = Task.Run(delegate
				{
					byte[] item = ImageConversion.EncodeToPNG(<>8__1.screenshot);
					Object.Destroy((Object)(object)<>8__1.screenshot);
					int num = Mathf.RoundToInt(<>8__1.<>4__this.cfgReplaySeconds.Value * (float)<>8__1.<>4__this.cfgBufferFPS.Value);
					if (<>8__1.<>4__this.frameBuffer.Count >= num)
					{
						<>8__1.<>4__this.frameBuffer.Dequeue();
					}
					<>8__1.<>4__this.frameBuffer.Enqueue(item);
				});
				<>1__state = 2;
				return true;
			case 2:
				<>1__state = -1;
				<>4__this.isCapturingFrame = false;
				return false;
			}
		}

		bool IEnumerator.MoveNext()
		{
			//ILSpy generated this explicit interface implementation from .override directive in MoveNext
			return this.MoveNext();
		}

		[DebuggerHidden]
		void IEnumerator.Reset()
		{
			throw new NotSupportedException();
		}
	}

	[CompilerGenerated]
	private sealed class <ExportCoroutine>d__36 : IEnumerator<object>, IDisposable, IEnumerator
	{
		private int <>1__state;

		private object <>2__current;

		public HeadcamReplay <>4__this;

		object IEnumerator<object>.Current
		{
			[DebuggerHidden]
			get
			{
				return <>2__current;
			}
		}

		object IEnumerator.Current
		{
			[DebuggerHidden]
			get
			{
				return <>2__current;
			}
		}

		[DebuggerHidden]
		public <ExportCoroutine>d__36(int <>1__state)
		{
			this.<>1__state = <>1__state;
		}

		[DebuggerHidden]
		void IDisposable.Dispose()
		{
			<>1__state = -2;
		}

		private bool MoveNext()
		{
			switch (<>1__state)
			{
			default:
				return false;
			case 0:
				<>1__state = -1;
				<>4__this.PrepareExport();
				<>2__current = <>4__this.FinalizeExportCoroutine();
				<>1__state = 1;
				return true;
			case 1:
				<>1__state = -1;
				<>4__this.PublicLogger.LogInfo((object)"Pre-death footage export complete.");
				return false;
			}
		}

		bool IEnumerator.MoveNext()
		{
			//ILSpy generated this explicit interface implementation from .override directive in MoveNext
			return this.MoveNext();
		}

		[DebuggerHidden]
		void IEnumerator.Reset()
		{
			throw new NotSupportedException();
		}
	}

	[CompilerGenerated]
	private sealed class <FinalizeExportCoroutine>d__39 : IEnumerator<object>, IDisposable, IEnumerator
	{
		private int <>1__state;

		private object <>2__current;

		public HeadcamReplay <>4__this;

		private <>c__DisplayClass39_0 <>8__1;

		private bool <useSystemPath>5__2;

		private int <i>5__3;

		private string <path>5__4;

		private <>c__DisplayClass39_1 <>8__5;

		private float <playbackFPS>5__6;

		object IEnumerator<object>.Current
		{
			[DebuggerHidden]
			get
			{
				return <>2__current;
			}
		}

		object IEnumerator.Current
		{
			[DebuggerHidden]
			get
			{
				return <>2__current;
			}
		}

		[DebuggerHidden]
		public <FinalizeExportCoroutine>d__39(int <>1__state)
		{
			this.<>1__state = <>1__state;
		}

		[DebuggerHidden]
		void IDisposable.Dispose()
		{
			<>8__1 = null;
			<path>5__4 = null;
			<>8__5 = null;
			<>1__state = -2;
		}

		private bool MoveNext()
		{
			int num = <>1__state;
			if (num != 0)
			{
				if (num != 1)
				{
					return false;
				}
				<>1__state = -1;
			}
			else
			{
				<>1__state = -1;
				<>8__1 = new <>c__DisplayClass39_0();
				<>8__1.<>4__this = <>4__this;
				<i>5__3 = 0;
				while (<i>5__3 < <>4__this.framePngsToExport.Count)
				{
					<path>5__4 = Path.Combine(<>4__this.tempPngFolder, $"{<i>5__3:D6}.png");
					File.WriteAllBytes(<path>5__4, <>4__this.framePngsToExport[<i>5__3]);
					<path>5__4 = null;
					<i>5__3++;
				}
				<>4__this.framePngsToExport.Clear();
				<useSystemPath>5__2 = <>4__this.ffmpegPath == "ffmpeg";
				<>8__1.execPath = <>4__this.ffmpegPath;
				if (!<useSystemPath>5__2 && !File.Exists(<>8__1.execPath))
				{
					<>4__this.PublicLogger.LogError((object)("FFmpeg not found at the expected path: " + <>4__this.ffmpegPath + ". Export skipped."));
					goto IL_02db;
				}
				<>8__5 = new <>c__DisplayClass39_1();
				<>8__5.CS$<>8__locals1 = <>8__1;
				if (!<useSystemPath>5__2)
				{
					<>8__5.CS$<>8__locals1.execPath = "\"" + <>8__5.CS$<>8__locals1.execPath + "\"";
				}
				<playbackFPS>5__6 = (float)<>4__this.cfgBufferFPS.Value * <>4__this.cfgPlaybackSpeed.Value;
				if (<playbackFPS>5__6 < 1f)
				{
					<playbackFPS>5__6 = 1f;
				}
				<>8__5.args = $"-framerate {<playbackFPS>5__6} -i \"{<>4__this.tempPngFolder}/%06d.png\" -c:v libx264 -pix_fmt yuv420p -y \"{<>4__this.exportFilePath}\"";
				<>4__this.PublicLogger.LogInfo((object)$"Starting FFmpeg process. Playback FPS: {<playbackFPS>5__6}, Arguments: {<>8__5.args}");
				<>8__5.finished = false;
				Task.Run(delegate
				{
					try
					{
						ProcessStartInfo startInfo = new ProcessStartInfo
						{
							FileName = <>8__5.CS$<>8__locals1.execPath,
							Arguments = <>8__5.args,
							UseShellExecute = false,
							CreateNoWindow = true
						};
						Process process = new Process
						{
							StartInfo = startInfo
						};
						process.Start();
						process.WaitForExit();
						<>8__5.finished = true;
					}
					catch (Exception ex)
					{
						<>8__5.CS$<>8__locals1.<>4__this.PublicLogger.LogError((object)("Failed to run FFmpeg: ApplicationName='" + <>8__5.CS$<>8__locals1.execPath + "', Arguments='" + <>8__5.args + "', Error= " + ex.Message));
						<>8__5.finished = true;
					}
				});
			}
			if (!<>8__5.finished)
			{
				<>2__current = null;
				<>1__state = 1;
				return true;
			}
			<>4__this.PublicLogger.LogInfo((object)"FFmpeg finished. Cleaning up.");
			<>8__5 = null;
			goto IL_02db;
			IL_02db:
			try
			{
				Directory.Delete(<>4__this.tempPngFolder, recursive: true);
			}
			catch
			{
			}
			<>4__this.PublicLogger.LogInfo((object)("Pre-death footage export complete. Saved to: " + <>4__this.exportFilePath));
			return false;
		}

		bool IEnumerator.MoveNext()
		{
			//ILSpy generated this explicit interface implementation from .override directive in MoveNext
			return this.MoveNext();
		}

		[DebuggerHidden]
		void IEnumerator.Reset()
		{
			throw new NotSupportedException();
		}
	}

	public const string PLUGIN_GUID = "com.yourname.headcamreplay";

	public const string PLUGIN_NAME = "Headcam Replay";

	public const string PLUGIN_VERSION = "1.1.3";

	private ConfigEntry<float> cfgReplaySeconds;

	private ConfigEntry<int> cfgBufferFPS;

	private ConfigEntry<string> cfgExportFolder;

	private ConfigEntry<float> cfgPlaybackSpeed;

	private Queue<byte[]> frameBuffer;

	internal float timeOfDeath = 0f;

	private const float POST_DEATH_DELAY = 2f;

	private List<byte[]> framePngsToExport;

	private string exportFilePath;

	private string tempPngFolder;

	private string ffmpegPath;

	private bool isCapturingFrame = false;

	internal static HeadcamReplay Instance { get; private set; }

	internal static Camera StaticPlayerCam { get; set; }

	internal static CameraNoise StaticCameraNoise { get; set; }

	public ManualLogSource PublicLogger => ((BaseUnityPlugin)this).Logger;

	private Camera PlayerCam => StaticPlayerCam;

	private void Awake()
	{
		//IL_004d: Unknown result type (might be due to invalid IL or missing references)
		//IL_0057: Expected O, but got Unknown
		//IL_0082: Unknown result type (might be due to invalid IL or missing references)
		//IL_008c: Expected O, but got Unknown
		//IL_00f6: Unknown result type (might be due to invalid IL or missing references)
		//IL_0100: Expected O, but got Unknown
		//IL_011b: Unknown result type (might be due to invalid IL or missing references)
		Instance = this;
		frameBuffer = new Queue<byte[]>();
		framePngsToExport = new List<byte[]>();
		cfgReplaySeconds = ((BaseUnityPlugin)this).Config.Bind<float>("General", "ReplaySeconds", 10f, new ConfigDescription("Seconds of pre-death footage", (AcceptableValueBase)(object)new AcceptableValueRange<float>(5f, 30f), Array.Empty<object>()));
		cfgBufferFPS = ((BaseUnityPlugin)this).Config.Bind<int>("Performance", "BufferFPS", 30, new ConfigDescription("Recording FPS", (AcceptableValueBase)(object)new AcceptableValueRange<int>(24, 60), Array.Empty<object>()));
		string text = Path.Combine(Application.dataPath, "..", "DeathVids");
		cfgExportFolder = ((BaseUnityPlugin)this).Config.Bind<string>("Export", "ExportFolder", text, "Folder for MP4s (full path or relative to game dir)");
		cfgPlaybackSpeed = ((BaseUnityPlugin)this).Config.Bind<float>("Export", "PlaybackSpeedMultiplier", 1f, new ConfigDescription("Playback speed multiplier (1.0 = normal, 0.5 = half speed/slow motion)", (AcceptableValueBase)(object)new AcceptableValueRange<float>(0.1f, 4f), Array.Empty<object>()));
		Directory.CreateDirectory(cfgExportFolder.Value);
		new Harmony("com.yourname.headcamreplay").PatchAll();
		PublicLogger.LogInfo((object)"Headcam Replay v1.1.3 loaded. Delayed full screen capture enabled.");
	}

	private void Update()
	{
		if ((Object)(object)PlayerCam == (Object)null)
		{
			TryFindPlayerComponentsFallback();
		}
		if ((Object)(object)PlayerCam != (Object)null && !isCapturingFrame && Time.frameCount % (60 / cfgBufferFPS.Value) == 0)
		{
			((MonoBehaviour)this).StartCoroutine(CaptureCurrentScreenFrame());
		}
		if (timeOfDeath > 0f && Time.time >= timeOfDeath + 2f)
		{
			StartExport();
			timeOfDeath = 0f;
		}
	}

	private void TryFindPlayerComponentsFallback()
	{
		if ((Object)(object)StaticCameraNoise == (Object)null)
		{
			StaticCameraNoise = Object.FindObjectOfType<CameraNoise>();
		}
		if ((Object)(object)StaticCameraNoise != (Object)null && (Object)(object)StaticPlayerCam == (Object)null)
		{
			StaticPlayerCam = ((Component)StaticCameraNoise).GetComponentInChildren<Camera>(true);
		}
	}

	[IteratorStateMachine(typeof(<CaptureCurrentScreenFrame>d__34))]
	private IEnumerator CaptureCurrentScreenFrame()
	{
		//yield-return decompiler failed: Unexpected instruction in Iterator.Dispose()
		return new <CaptureCurrentScreenFrame>d__34(0)
		{
			<>4__this = this
		};
	}

	internal void StartExport()
	{
		if ((Object)(object)PlayerCam == (Object)null)
		{
			TryFindPlayerComponentsFallback();
			if ((Object)(object)PlayerCam == (Object)null)
			{
				PublicLogger.LogError((object)"Cannot start export: Player Camera is null. Export aborted.");
				return;
			}
		}
		framePngsToExport.Clear();
		foreach (byte[] item in frameBuffer)
		{
			framePngsToExport.Add(item);
		}
		frameBuffer.Clear();
		((MonoBehaviour)this).StartCoroutine(ExportCoroutine());
		PublicLogger.LogInfo((object)$"Pre-death footage export initiated. Capturing {framePngsToExport.Count} frames.");
	}

	[IteratorStateMachine(typeof(<ExportCoroutine>d__36))]
	private IEnumerator ExportCoroutine()
	{
		//yield-return decompiler failed: Unexpected instruction in Iterator.Dispose()
		return new <ExportCoroutine>d__36(0)
		{
			<>4__this = this
		};
	}

	private void PrepareExport()
	{
		string text = "Player";
		if ((Object)(object)StaticCameraNoise != (Object)null)
		{
			text = ((Object)((Component)StaticCameraNoise).gameObject.transform.root).name.Replace("(Clone)", "").Trim();
		}
		string text2 = DateTime.Now.ToString("yyyyMMdd_HHmmss");
		string path = text + "_" + text2 + ".mp4";
		exportFilePath = Path.Combine(cfgExportFolder.Value, path);
		tempPngFolder = Path.Combine(Path.GetTempPath(), "HeadcamReplay_" + Guid.NewGuid().ToString("N").Substring(0, 8));
		Directory.CreateDirectory(tempPngFolder);
		DownloadFFmpeg();
	}

	private void DownloadFFmpeg()
	{
		string directoryName = Path.GetDirectoryName(typeof(HeadcamReplay).Assembly.Location);
		ffmpegPath = Path.Combine(directoryName, "ffmpeg.exe");
		if (File.Exists(ffmpegPath))
		{
			PublicLogger.LogInfo((object)("FFmpeg found locally: " + ffmpegPath));
			return;
		}
		PublicLogger.LogWarning((object)"FFmpeg not found next to mod DLL. Attempting to use system PATH (command: 'ffmpeg').");
		ffmpegPath = "ffmpeg";
	}

	[IteratorStateMachine(typeof(<FinalizeExportCoroutine>d__39))]
	private IEnumerator FinalizeExportCoroutine()
	{
		//yield-return decompiler failed: Unexpected instruction in Iterator.Dispose()
		return new <FinalizeExportCoroutine>d__39(0)
		{
			<>4__this = this
		};
	}
}
[HarmonyPatch(typeof(PlayerLocalCamera), "Update")]
public static class CameraReferencePatch
{
	private static readonly FieldInfo PhotonViewField = AccessTools.Field(typeof(PlayerLocalCamera), "photonView");

	public static void Postfix(PlayerLocalCamera __instance)
	{
		if ((Object)(object)HeadcamReplay.Instance == (Object)null || (Object)(object)HeadcamReplay.StaticPlayerCam != (Object)null)
		{
			return;
		}
		ManualLogSource publicLogger = HeadcamReplay.Instance.PublicLogger;
		object? obj = PhotonViewField?.GetValue(__instance);
		PhotonView val = (PhotonView)((obj is PhotonView) ? obj : null);
		if (!((Object)(object)val == (Object)null) && val.IsMine && (Object)(object)CameraNoise.Instance != (Object)null)
		{
			HeadcamReplay.StaticCameraNoise = CameraNoise.Instance;
			HeadcamReplay.StaticPlayerCam = ((Component)CameraNoise.Instance).GetComponentInChildren<Camera>(true);
			if ((Object)(object)HeadcamReplay.StaticPlayerCam == (Object)null)
			{
				publicLogger.LogError((object)"Camera component not found on CameraNoise children.");
			}
			else
			{
				publicLogger.LogInfo((object)"Successfully cached Camera references using CameraNoise.");
			}
		}
	}
}
[HarmonyPatch(typeof(PlayerHealth), "Death")]
public static class PlayerDeathPatch
{
	public static void Postfix()
	{
		if ((Object)(object)HeadcamReplay.Instance != (Object)null && HeadcamReplay.Instance.timeOfDeath == 0f)
		{
			HeadcamReplay.Instance.timeOfDeath = Time.time;
			HeadcamReplay.Instance.PublicLogger.LogInfo((object)"Death detected. Recording will continue for 2 seconds.");
		}
	}
}