Please disclose if any significant portion of your mod was created using AI tools by adding the 'AI Generated' category. Failing to do so may result in the mod being removed from Thunderstore.
Decompiled source of OBS Sync v1.0.0
Nicole.OBSSync.dll
Decompiled 2 years agousing System; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Net.WebSockets; using System.Reflection; using System.Reflection.Emit; using System.Runtime.CompilerServices; using System.Runtime.Versioning; using System.Security; using System.Security.Cryptography; using System.Security.Permissions; using System.Text; using System.Threading; using System.Threading.Tasks; using BepInEx; using BepInEx.Configuration; using BepInEx.Logging; using GameNetcodeStuff; using HarmonyLib; using Microsoft.CodeAnalysis; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using OBSSync.Websocket; using Unity.Netcode; using UnityEngine; using UnityEngine.InputSystem; using UnityEngine.InputSystem.Controls; [assembly: CompilationRelaxations(8)] [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)] [assembly: TargetFramework(".NETStandard,Version=v2.1", FrameworkDisplayName = ".NET Standard 2.1")] [assembly: AssemblyCompany("Nicole")] [assembly: AssemblyConfiguration("Release")] [assembly: AssemblyDescription("Automatically control your OBS and create timestamp logs of your games for easier video editing.")] [assembly: AssemblyFileVersion("1.0.0.0")] [assembly: AssemblyInformationalVersion("1.0.0")] [assembly: AssemblyProduct("Nicole.OBSSync")] [assembly: AssemblyTitle("OBS Sync")] [assembly: SecurityPermission(SecurityAction.RequestMinimum, SkipVerification = true)] [assembly: AssemblyVersion("1.0.0.0")] [module: UnverifiableCode] [module: RefSafetyRules(11)] namespace Microsoft.CodeAnalysis { [CompilerGenerated] [Microsoft.CodeAnalysis.Embedded] internal sealed class EmbeddedAttribute : Attribute { } } namespace System.Runtime.CompilerServices { [CompilerGenerated] [Microsoft.CodeAnalysis.Embedded] [AttributeUsage(AttributeTargets.Class | AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Event | AttributeTargets.Parameter | AttributeTargets.ReturnValue | AttributeTargets.GenericParameter, AllowMultiple = false, Inherited = false)] internal sealed class NullableAttribute : Attribute { public readonly byte[] NullableFlags; public NullableAttribute(byte P_0) { NullableFlags = new byte[1] { P_0 }; } public NullableAttribute(byte[] P_0) { NullableFlags = P_0; } } [CompilerGenerated] [Microsoft.CodeAnalysis.Embedded] [AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Method | AttributeTargets.Interface | AttributeTargets.Delegate, AllowMultiple = false, Inherited = false)] internal sealed class NullableContextAttribute : Attribute { public readonly byte Flag; public NullableContextAttribute(byte P_0) { Flag = P_0; } } [CompilerGenerated] [Microsoft.CodeAnalysis.Embedded] [AttributeUsage(AttributeTargets.Module, AllowMultiple = false, Inherited = false)] internal sealed class RefSafetyRulesAttribute : Attribute { public readonly int Version; public RefSafetyRulesAttribute(int P_0) { Version = P_0; } } } namespace BepInEx { [AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)] [Conditional("CodeGeneration")] internal sealed class BepInAutoPluginAttribute : Attribute { public BepInAutoPluginAttribute(string id = null, string name = null, string version = null) { } } } namespace BepInEx.Preloader.Core.Patching { [AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)] [Conditional("CodeGeneration")] internal sealed class PatcherAutoPluginAttribute : Attribute { public PatcherAutoPluginAttribute(string id = null, string name = null, string version = null) { } } } namespace OBSSync { [BepInPlugin("Nicole.OBSSync", "OBS Sync", "1.0.0")] public class ObsSyncPlugin : BaseUnityPlugin { private readonly ObsWebsocket _obs = new ObsWebsocket(); private SelectableLevel? _lastPlayedLevel; private StreamWriter? _currentTimestampLog; private DateTime _currentLogStart; private ConfigEntry<string> _configObsWebsocketAddress; private ConfigEntry<string> _configObsWebsocketPassword; private ConfigEntry<bool> _configAutoStartStop; private ConfigEntry<bool> _configAutoSplit; private ConfigEntry<Key> _configManualEventKey; public const string Id = "Nicole.OBSSync"; public static ObsSyncPlugin Instance { get; private set; } internal static ManualLogSource Logger { get; private set; } internal static Harmony? Harmony { get; set; } private string? LastPlanetName { get { if ((Object)(object)_lastPlayedLevel == (Object)null) { return null; } int num = _lastPlayedLevel.PlanetName.IndexOf(' '); return _lastPlayedLevel.PlanetName.Substring(num + 1); } } public static string Name => "OBS Sync"; public static string Version => "1.0.0"; private void Awake() { Logger = ((BaseUnityPlugin)this).Logger; Instance = this; Harmony = Harmony.CreateAndPatchAll(typeof(Patches), "Nicole.OBSSync"); BuildConfig(); InitializeObsClient(); } private void OnDestroy() { _currentTimestampLog?.Close(); } private void Update() { //IL_000b: Unknown result type (might be due to invalid IL or missing references) if (((ButtonControl)Keyboard.current[_configManualEventKey.Value]).wasPressedThisFrame) { WriteTimestamppedEvent("Manual event"); } } private void InitializeObsClient() { _obs.Connected += ObsConnected; _obs.Disconnected += ObsDisconnected; _obs.RecordStateChanged += delegate(RecordStateChangedEventData data) { if (!(data.OutputState != "OBS_WEBSOCKET_OUTPUT_STARTED")) { string directoryName = Path.GetDirectoryName(data.OutputPath); string fileNameWithoutExtension = Path.GetFileNameWithoutExtension(data.OutputPath); string path = Path.Combine(directoryName, fileNameWithoutExtension + ".txt"); _currentTimestampLog = new StreamWriter(path); _currentTimestampLog.AutoFlush = true; _currentLogStart = DateTime.Now; } }; DoConnect(); } private void DoConnect() { try { _obs.Connect("ws://" + _configObsWebsocketAddress.Value, _configObsWebsocketPassword.Value); } catch (Exception ex) { Logger.LogError((object)ex); } } private void ObsConnected() { Logger.LogInfo((object)"Connected to OBS websocket!"); } private void ObsDisconnected(string reason) { Logger.LogWarning((object)("Disconnected from OBS websocket: " + reason + ". Retrying in 5 seconds...")); Task.Run(async delegate { await Task.Delay(5000); DoConnect(); }); } private void RenameLogFile(string outputFilePath, string filenameSuffix) { string directoryName = Path.GetDirectoryName(outputFilePath); string extension = Path.GetExtension(outputFilePath); string fileNameWithoutExtension = Path.GetFileNameWithoutExtension(outputFilePath); string destFileName = Path.Combine(directoryName, fileNameWithoutExtension + "_" + filenameSuffix + extension); File.Move(outputFilePath, destFileName); if (_currentTimestampLog != null) { _currentTimestampLog.Close(); _currentTimestampLog = null; string sourceFileName = Path.Combine(directoryName, fileNameWithoutExtension + ".txt"); string destFileName2 = Path.Combine(directoryName, fileNameWithoutExtension + "_" + filenameSuffix + ".txt"); File.Move(sourceFileName, destFileName2); } } public async void StartRecording() { await _obs.MakeRequestAsync(new StartRecordRequest()); } public async void StopRecording(string filenameSuffix) { string filenameSuffix2 = filenameSuffix; if ((await _obs.MakeRequestAsync(new StopRecordRequest())).Status.Success) { _obs.RecordStateChanged += WaitForRecordingStopped; } void WaitForRecordingStopped(RecordStateChangedEventData e) { if (!(e.OutputState != "OBS_WEBSOCKET_OUTPUT_STOPPED")) { _obs.RecordStateChanged -= WaitForRecordingStopped; RenameLogFile(e.OutputPath, filenameSuffix2); } } } public async Task SplitRecording(string filenameSuffix) { string filenameSuffix2 = filenameSuffix; SemaphoreSlim waitHandle; if ((await _obs.MakeRequestAsync(new StopRecordRequest())).Status.Success) { waitHandle = new SemaphoreSlim(0, 1); _obs.RecordStateChanged += WaitForRecordingStopped; await waitHandle.WaitAsync(); await Task.Yield(); } else { await _obs.MakeRequestAsync(new StartRecordRequest()); } async void WaitForRecordingStopped(RecordStateChangedEventData e) { if (e.OutputState == "OBS_WEBSOCKET_OUTPUT_STOPPED") { RenameLogFile(e.OutputPath, filenameSuffix2); await Task.Delay(150); await _obs.MakeRequestAsync(new StartRecordRequest()); } else if (e.OutputState == "OBS_WEBSOCKET_OUTPUT_STARTED") { _obs.RecordStateChanged -= WaitForRecordingStopped; waitHandle.Release(); } } } internal void JoinedGame() { _lastPlayedLevel = null; if (_configAutoStartStop.Value) { StartRecording(); } } internal void LeftGame() { if (_configAutoStartStop.Value) { string filenameSuffix = LastPlanetName ?? "end"; StopRecording(filenameSuffix); } } internal async void RoundStarting() { if (_configAutoSplit.Value) { string filenameSuffix = LastPlanetName ?? "start"; await SplitRecording(filenameSuffix); } else { WriteTimestamppedEvent(""); } _lastPlayedLevel = StartOfRound.Instance.currentLevel; WriteTimestamppedEvent("Landing on " + LastPlanetName); } internal void RoundFinished() { WriteTimestamppedEvent("Left moon, now in orbit"); } internal void WriteTimestamppedEvent(string what) { TimeSpan timeSpan = DateTime.Now - _currentLogStart; string text = $"[{timeSpan:hh':'mm':'ss}] {what}"; Logger.LogDebug((object)text); if (_currentTimestampLog == null) { Logger.LogWarning((object)"Trying to write stuff but no logfile is opened... start a recording!"); } else if (string.IsNullOrEmpty(what)) { _currentTimestampLog.WriteLine(); } else { _currentTimestampLog.WriteLine(text); } } private void BuildConfig() { _configObsWebsocketAddress = ((BaseUnityPlugin)this).Config.Bind<string>("Connection", "WebsocketAddress", "[::1]:4455", "IP address / port of the computer running OBS"); _configObsWebsocketPassword = ((BaseUnityPlugin)this).Config.Bind<string>("Connection", "WebsocketPassword", "", "Password to connect to OBS's websocket"); _configAutoStartStop = ((BaseUnityPlugin)this).Config.Bind<bool>("Recording", "AutoStartStop", true, "Automatically start recording when you start or join a game and automatically stop recording when you leave a game."); _configAutoSplit = ((BaseUnityPlugin)this).Config.Bind<bool>("Recording", "AutoSplit", true, "Automatically stop and start a new recording between moons"); _configManualEventKey = ((BaseUnityPlugin)this).Config.Bind<Key>("Recording", "ManualEventKey", (Key)67, "The key that will add a manual event into the timestamp log"); } } internal static class Patches { [HarmonyPatch(typeof(StartOfRound), "ResetPlayersLoadedValueClientRpc")] [HarmonyTranspiler] public static IEnumerable<CodeInstruction> OnNewRoundStarted(IEnumerable<CodeInstruction> instructions) { //IL_0002: Unknown result type (might be due to invalid IL or missing references) //IL_000c: Expected O, but got Unknown //IL_002a: Unknown result type (might be due to invalid IL or missing references) //IL_0030: Expected O, but got Unknown return SkipRpcCrap(new CodeMatcher(instructions, (ILGenerator)null)).Insert((CodeInstruction[])(object)new CodeInstruction[1] { new CodeInstruction(OpCodes.Call, (object)new Action(ActualFunction).Method) }).InstructionEnumeration(); static void ActualFunction() { ObsSyncPlugin.Instance.RoundStarting(); } } [HarmonyPatch(typeof(StartOfRound), "EndOfGame")] [HarmonyPrefix] public static void StartOfRound_EndOfGame_Prefix() { ObsSyncPlugin.Instance.RoundFinished(); } [HarmonyPatch(typeof(StartOfRound), "Start")] [HarmonyPrefix] public static void OnGameJoined() { ObsSyncPlugin.Instance.JoinedGame(); } [HarmonyPatch(typeof(GameNetworkManager), "StartDisconnect")] [HarmonyPrefix] public static void OnGameLeave() { ObsSyncPlugin.Instance.LeftGame(); } [HarmonyPatch(typeof(PlayerControllerB), "KillPlayerClientRpc")] [HarmonyTranspiler] public static IEnumerable<CodeInstruction> OnPlayerKilled(IEnumerable<CodeInstruction> instructions) { //IL_0002: Unknown result type (might be due to invalid IL or missing references) //IL_000c: Expected O, but got Unknown //IL_001a: Unknown result type (might be due to invalid IL or missing references) //IL_0020: Expected O, but got Unknown //IL_002d: Unknown result type (might be due to invalid IL or missing references) //IL_0033: Expected O, but got Unknown //IL_004b: Unknown result type (might be due to invalid IL or missing references) //IL_0051: Expected O, but got Unknown return SkipRpcCrap(new CodeMatcher(instructions, (ILGenerator)null)).Insert((CodeInstruction[])(object)new CodeInstruction[3] { new CodeInstruction(OpCodes.Ldarg_1, (object)null), new CodeInstruction(OpCodes.Ldarg, (object)4), new CodeInstruction(OpCodes.Call, (object)new Action<int, int>(ActualFunction).Method) }).InstructionEnumeration(); static void ActualFunction(int playerId, int causeOfDeath) { string playerUsername = StartOfRound.Instance.allPlayerScripts[playerId].playerUsername; ObsSyncPlugin.Instance.WriteTimestamppedEvent($"Player {playerUsername} died ({(object)(CauseOfDeath)causeOfDeath})"); } } [HarmonyPatch(typeof(EnemyAI), "KillEnemy")] [HarmonyPostfix] public static void OnEnemyKilled(EnemyAI __instance) { if (!((Object)((Component)__instance).gameObject).name.Contains("Doublewinged")) { ObsSyncPlugin.Instance.WriteTimestamppedEvent("Enemy " + ((Object)((Component)__instance).gameObject).name + " died"); } } [HarmonyPatch(typeof(FlowermanAI), "EnterAngerModeClientRpc")] [HarmonyTranspiler] public static IEnumerable<CodeInstruction> OnBrackenAngered(IEnumerable<CodeInstruction> instructions) { //IL_0002: Unknown result type (might be due to invalid IL or missing references) //IL_000c: Expected O, but got Unknown //IL_001a: Unknown result type (might be due to invalid IL or missing references) //IL_0020: Expected O, but got Unknown //IL_0038: Unknown result type (might be due to invalid IL or missing references) //IL_003e: Expected O, but got Unknown return SkipRpcCrap(new CodeMatcher(instructions, (ILGenerator)null)).Insert((CodeInstruction[])(object)new CodeInstruction[2] { new CodeInstruction(OpCodes.Ldarg_0, (object)null), new CodeInstruction(OpCodes.Call, (object)new Action<FlowermanAI>(ActualFunction).Method) }).InstructionEnumeration(); static void ActualFunction(FlowermanAI self) { if ((Object)(object)self.lookAtPlayer != (Object)null) { ObsSyncPlugin.Instance.WriteTimestamppedEvent("Bracken angered towards " + self.lookAtPlayer.playerUsername); } } } [HarmonyPatch(typeof(JesterAI), "Update")] [HarmonyTranspiler] public static IEnumerable<CodeInstruction> OnJesterStartWinding(IEnumerable<CodeInstruction> instructions) { //IL_0019: Unknown result type (might be due to invalid IL or missing references) //IL_0033: Unknown result type (might be due to invalid IL or missing references) //IL_0039: Expected O, but got Unknown //IL_0047: Unknown result type (might be due to invalid IL or missing references) //IL_004d: Expected O, but got Unknown //IL_005b: Unknown result type (might be due to invalid IL or missing references) //IL_0061: Expected O, but got Unknown //IL_0074: Unknown result type (might be due to invalid IL or missing references) //IL_007a: Expected O, but got Unknown //IL_0092: Unknown result type (might be due to invalid IL or missing references) //IL_0098: Expected O, but got Unknown FieldInfo field = typeof(JesterAI).GetField("previousState", BindingFlags.Instance | BindingFlags.NonPublic); return new CodeMatcher(instructions, (ILGenerator)null).MatchForward(false, (CodeMatch[])(object)new CodeMatch[3] { new CodeMatch((OpCode?)OpCodes.Ldarg_0, (object)null, (string)null), new CodeMatch((OpCode?)OpCodes.Ldc_I4_1, (object)null, (string)null), new CodeMatch((OpCode?)OpCodes.Stfld, (object)field, (string)null) }).Insert((CodeInstruction[])(object)new CodeInstruction[2] { new CodeInstruction(OpCodes.Ldarg_0, (object)null), new CodeInstruction(OpCodes.Call, (object)new Action<JesterAI>(ActualFunction).Method) }).InstructionEnumeration(); static void ActualFunction(JesterAI self) { ObsSyncPlugin.Instance.WriteTimestamppedEvent("Jester started winding"); } } [HarmonyPatch(typeof(HoarderBugAI), "Update")] [HarmonyTranspiler] public static IEnumerable<CodeInstruction> OnLootBugAngered(IEnumerable<CodeInstruction> instructions) { //IL_0019: Unknown result type (might be due to invalid IL or missing references) //IL_0033: Unknown result type (might be due to invalid IL or missing references) //IL_0039: Expected O, but got Unknown //IL_0047: Unknown result type (might be due to invalid IL or missing references) //IL_004d: Expected O, but got Unknown //IL_005b: Unknown result type (might be due to invalid IL or missing references) //IL_0061: Expected O, but got Unknown //IL_0074: Unknown result type (might be due to invalid IL or missing references) //IL_007a: Expected O, but got Unknown //IL_0092: Unknown result type (might be due to invalid IL or missing references) //IL_0098: Expected O, but got Unknown FieldInfo field = typeof(HoarderBugAI).GetField("inChase", BindingFlags.Instance | BindingFlags.NonPublic); return new CodeMatcher(instructions, (ILGenerator)null).MatchForward(false, (CodeMatch[])(object)new CodeMatch[3] { new CodeMatch((OpCode?)OpCodes.Ldarg_0, (object)null, (string)null), new CodeMatch((OpCode?)OpCodes.Ldc_I4_1, (object)null, (string)null), new CodeMatch((OpCode?)OpCodes.Stfld, (object)field, (string)null) }).Insert((CodeInstruction[])(object)new CodeInstruction[2] { new CodeInstruction(OpCodes.Ldarg_0, (object)null), new CodeInstruction(OpCodes.Call, (object)new Action<HoarderBugAI>(ActualFunction).Method) }).InstructionEnumeration(); static void ActualFunction(HoarderBugAI self) { ObsSyncPlugin.Instance.WriteTimestamppedEvent("Loot bug angered by " + ((EnemyAI)self).targetPlayer.playerUsername); } } [HarmonyPatch(typeof(StunGrenadeItem), "ExplodeStunGrenade")] [HarmonyTranspiler] public static IEnumerable<CodeInstruction> OnEggExploded(IEnumerable<CodeInstruction> instructions) { //IL_0019: Unknown result type (might be due to invalid IL or missing references) //IL_0033: Unknown result type (might be due to invalid IL or missing references) //IL_0039: Expected O, but got Unknown //IL_0047: Unknown result type (might be due to invalid IL or missing references) //IL_004d: Expected O, but got Unknown //IL_005b: Unknown result type (might be due to invalid IL or missing references) //IL_0061: Expected O, but got Unknown //IL_0074: Unknown result type (might be due to invalid IL or missing references) //IL_007a: Expected O, but got Unknown //IL_0092: Unknown result type (might be due to invalid IL or missing references) //IL_0098: Expected O, but got Unknown FieldInfo field = typeof(StunGrenadeItem).GetField("hasExploded", BindingFlags.Instance | BindingFlags.Public); return new CodeMatcher(instructions, (ILGenerator)null).MatchForward(true, (CodeMatch[])(object)new CodeMatch[3] { new CodeMatch((OpCode?)OpCodes.Ldarg_0, (object)null, (string)null), new CodeMatch((OpCode?)OpCodes.Ldc_I4_1, (object)null, (string)null), new CodeMatch((OpCode?)OpCodes.Stfld, (object)field, (string)null) }).Insert((CodeInstruction[])(object)new CodeInstruction[2] { new CodeInstruction(OpCodes.Ldarg_0, (object)null), new CodeInstruction(OpCodes.Call, (object)new Action<StunGrenadeItem>(ActualFunction).Method) }).InstructionEnumeration(); static void ActualFunction(StunGrenadeItem self) { if (((Object)self).name.Contains("Easter egg")) { ObsSyncPlugin.Instance.WriteTimestamppedEvent("Easter egg exploded"); } else { ObsSyncPlugin.Instance.WriteTimestamppedEvent("Stun grenade exploded"); } } } [HarmonyPatch(typeof(PlayerControllerB), "DamagePlayerClientRpc")] [HarmonyTranspiler] public static IEnumerable<CodeInstruction> OnPlayerDamaged(IEnumerable<CodeInstruction> instructions) { //IL_0002: Unknown result type (might be due to invalid IL or missing references) //IL_000c: Expected O, but got Unknown //IL_001a: Unknown result type (might be due to invalid IL or missing references) //IL_0020: Expected O, but got Unknown //IL_0038: Unknown result type (might be due to invalid IL or missing references) //IL_003e: Expected O, but got Unknown return SkipRpcCrap(new CodeMatcher(instructions, (ILGenerator)null)).Insert((CodeInstruction[])(object)new CodeInstruction[2] { new CodeInstruction(OpCodes.Ldarg_0, (object)null), new CodeInstruction(OpCodes.Call, (object)new Action<PlayerControllerB>(ActualFunction).Method) }).InstructionEnumeration(); static void ActualFunction(PlayerControllerB self) { ObsSyncPlugin.Instance.WriteTimestamppedEvent("Player " + self.playerUsername + " took damage"); } } [HarmonyPatch(typeof(PlayerControllerB), "JumpToFearLevel")] [HarmonyTranspiler] public static IEnumerable<CodeInstruction> OnJumpToFearLevel(IEnumerable<CodeInstruction> instructions) { //IL_0002: Unknown result type (might be due to invalid IL or missing references) //IL_0021: Unknown result type (might be due to invalid IL or missing references) //IL_0027: Expected O, but got Unknown //IL_004a: Unknown result type (might be due to invalid IL or missing references) //IL_0050: Expected O, but got Unknown return new CodeMatcher(instructions, (ILGenerator)null).End().MatchBack(false, (CodeMatch[])(object)new CodeMatch[1] { new CodeMatch((OpCode?)OpCodes.Ret, (object)null, (string)null) }).Insert((CodeInstruction[])(object)new CodeInstruction[1] { new CodeInstruction(OpCodes.Call, (object)new Action(ActualFunction).Method) }) .InstructionEnumeration(); static void ActualFunction() { float fearLevel = StartOfRound.Instance.fearLevel; if (!(fearLevel > 0.9f)) { if (!(fearLevel > 0.75f)) { if (fearLevel > 0.4f) { ObsSyncPlugin.Instance.WriteTimestamppedEvent("Moderate fear event"); } else { ObsSyncPlugin.Instance.WriteTimestamppedEvent("Low fear event"); } } else { ObsSyncPlugin.Instance.WriteTimestamppedEvent("High fear event"); } } else { ObsSyncPlugin.Instance.WriteTimestamppedEvent("Extreme fear event"); } } } [HarmonyPatch(typeof(HUDManager), "AddTextToChatOnServer")] [HarmonyPrefix] public static bool OnChatMessage(string chatMessage, int playerId) { if (playerId != (int)GameNetworkManager.Instance.localPlayerController.playerClientId) { return true; } if (!chatMessage.StartsWith("!mark ")) { return true; } string text = chatMessage.Substring(6); ObsSyncPlugin.Instance.WriteTimestamppedEvent("Manual Event: " + text); return false; } private static CodeMatcher SkipRpcCrap(this CodeMatcher matcher) { //IL_0031: Unknown result type (might be due to invalid IL or missing references) //IL_0037: Expected O, but got Unknown //IL_0045: Unknown result type (might be due to invalid IL or missing references) //IL_004b: Expected O, but got Unknown //IL_006f: Unknown result type (might be due to invalid IL or missing references) //IL_0075: Expected O, but got Unknown //IL_0083: Unknown result type (might be due to invalid IL or missing references) //IL_0089: Expected O, but got Unknown FieldInfo field = typeof(NetworkBehaviour).GetField("__rpc_exec_stage", BindingFlags.Instance | BindingFlags.NonPublic); for (int i = 0; i < 2; i++) { matcher.MatchForward(true, (CodeMatch[])(object)new CodeMatch[2] { new CodeMatch((OpCode?)OpCodes.Ldarg_0, (object)null, (string)null), new CodeMatch((OpCode?)OpCodes.Ldfld, (object)field, (string)null) }); } matcher.MatchForward(true, (CodeMatch[])(object)new CodeMatch[2] { new CodeMatch((OpCode?)OpCodes.Ret, (object)null, (string)null), new CodeMatch((OpCode?)OpCodes.Nop, (object)null, (string)null) }); matcher.Advance(1); matcher.ThrowIfInvalid("Could not match for rpc stuff"); return matcher; } } } namespace OBSSync.Websocket { public class RecordStateChangedEventData { public const string StateStarting = "OBS_WEBSOCKET_OUTPUT_STARTING"; public const string StateStarted = "OBS_WEBSOCKET_OUTPUT_STARTED"; public const string StateStopping = "OBS_WEBSOCKET_OUTPUT_STOPPING"; public const string StateStopped = "OBS_WEBSOCKET_OUTPUT_STOPPED"; [JsonProperty(PropertyName = "outputActive")] public bool OutputActive { get; set; } [JsonProperty(PropertyName = "outputPath")] public string? OutputPath { get; set; } [JsonProperty(PropertyName = "outputState")] public string OutputState { get; set; } } public struct ObsRequestId { internal ulong Id { get; set; } } public abstract class ObsRequest { public string RequestType { get; } protected ObsRequest(string requestType) { RequestType = requestType; } public JObject GetRequestBody(string requestId) { //IL_0000: Unknown result type (might be due to invalid IL or missing references) //IL_0005: Unknown result type (might be due to invalid IL or missing references) //IL_001b: Unknown result type (might be due to invalid IL or missing references) //IL_002d: Expected O, but got Unknown JObject val = new JObject { ["requestType"] = JToken.op_Implicit(RequestType), ["requestId"] = JToken.op_Implicit(requestId) }; JObject requestData = GetRequestData(); if (requestData != null) { val["requestData"] = (JToken)(object)requestData; } return val; } protected virtual JObject? GetRequestData() { return null; } public abstract ObsRequestResponse MakeResponseObject(); } public abstract class ObsRequest<TResponse> : ObsRequest where TResponse : ObsRequestResponse, new() { public override ObsRequestResponse MakeResponseObject() { return new TResponse(); } protected ObsRequest(string requestType) : base(requestType) { } } public class ObsRequestResponse { public class ResponseStatus { public bool Success { get; } public int Code { get; } public string Comment { get; } public ResponseStatus(bool success, int code, string comment) { Success = success; Code = code; Comment = comment; base..ctor(); } } public ResponseStatus Status { get; private set; } public void SetRequestResponseBody(JObject body) { JObject val = Extensions.Value<JObject>((IEnumerable<JToken>)body["requestStatus"]); bool success = Extensions.Value<bool>((IEnumerable<JToken>)val["result"]); int code = Extensions.Value<int>((IEnumerable<JToken>)val["code"]); JToken obj = val["comment"]; string comment = ((obj != null) ? Extensions.Value<string>((IEnumerable<JToken>)obj) : null) ?? string.Empty; Status = new ResponseStatus(success, code, comment); if (body.ContainsKey("responseData")) { SetResponseData(Extensions.Value<JObject>((IEnumerable<JToken>)body["responseData"])); } } protected virtual void SetResponseData(JObject data) { } } public class ObsWebsocket { private class ObsMessage { [JsonProperty(PropertyName = "op")] public ObsMessageOpcode Op { get; set; } [JsonProperty(PropertyName = "d")] public JObject Data { get; set; } = new JObject(); } private enum ObsMessageOpcode { Hello = 0, Identify = 1, Identified = 2, ReIdentify = 3, Event = 5, Request = 6, RequestResponse = 7, RequestBatch = 8, RequestBatchResponse = 9 } private ClientWebSocket WebSocket { get; set; } = new ClientWebSocket(); private string? Password { get; set; } private ulong NextRequestId { get; set; } private Dictionary<ulong, (ObsRequest, TaskCompletionSource<object>)> SentRequests { get; } = new Dictionary<ulong, (ObsRequest, TaskCompletionSource<object>)>(); public event Action? Connected; public event Action<string>? Disconnected; public event Action<RecordStateChangedEventData>? RecordStateChanged; public async void Connect(string websocketAddress, string password) { Password = password; if (WebSocket.State != 0) { WebSocket = new ClientWebSocket(); } try { await WebSocket.ConnectAsync(new Uri(websocketAddress), default(CancellationToken)); } catch (Exception ex) { this.Disconnected?.Invoke(ex.Message); return; } byte[] bytes = new byte[2048]; WebSocketReceiveResult webSocketReceiveResult; while (true) { webSocketReceiveResult = await WebSocket.ReceiveAsync(bytes, default(CancellationToken)); if (!webSocketReceiveResult.EndOfMessage) { ObsSyncPlugin.Logger.LogError((object)"Message was larger than 2048 bytes, bug the author about this."); return; } if (webSocketReceiveResult.CloseStatus.HasValue) { break; } string @string = Encoding.UTF8.GetString(bytes, 0, webSocketReceiveResult.Count); ObsSyncPlugin.Logger.LogDebug((object)("Recv: " + @string)); ObsMessage obsMessage = JsonConvert.DeserializeObject<ObsMessage>(@string); if (obsMessage != null) { HandleMessage(obsMessage); } } DisconnectInternal(); this.Disconnected?.Invoke(webSocketReceiveResult.CloseStatusDescription); } public void Disconnect() { DisconnectInternal(); } public async Task<TResponse> MakeRequestAsync<TResponse>(ObsRequest<TResponse> request) where TResponse : ObsRequestResponse, new() { ObsRequestId obsRequestId = default(ObsRequestId); obsRequestId.Id = NextRequestId++; ObsRequestId obsRequestId2 = obsRequestId; SendMessage(new ObsMessage { Op = ObsMessageOpcode.Request, Data = request.GetRequestBody(obsRequestId2.Id.ToString()) }); TaskCompletionSource<object> taskCompletionSource = new TaskCompletionSource<object>(); SentRequests[obsRequestId2.Id] = (request, taskCompletionSource); return (TResponse)(await taskCompletionSource.Task); } private void HandleMessage(ObsMessage msg) { switch (msg.Op) { case ObsMessageOpcode.Hello: HandleHello(msg.Data); break; case ObsMessageOpcode.Identified: HandleIdentified(msg.Data); break; case ObsMessageOpcode.Event: HandleEvent(msg.Data); break; case ObsMessageOpcode.RequestResponse: HandleRequestResponse(msg.Data); break; default: ObsSyncPlugin.Logger.LogWarning((object)("Received unknown or unsupported opcode from OBS websocket: " + msg.Op)); break; } } private void HandleHello(JObject data) { ObsMessage obsMessage = new ObsMessage(); obsMessage.Op = ObsMessageOpcode.Identify; obsMessage.Data["rpcVersion"] = JToken.op_Implicit(1); if (data.ContainsKey("authentication")) { if (string.IsNullOrEmpty(Password)) { DisconnectInternal(); this.Disconnected?.Invoke("A password is required and not provided"); return; } JObject val = Extensions.Value<JObject>((IEnumerable<JToken>)data["authentication"]); string challenge = Extensions.Value<string>((IEnumerable<JToken>)val["challenge"]); string salt = Extensions.Value<string>((IEnumerable<JToken>)val["salt"]); obsMessage.Data["authentication"] = JToken.op_Implicit(CreateAuthenticationData(challenge, salt)); } SendMessage(obsMessage); } private void HandleIdentified(JObject data) { this.Connected?.Invoke(); } private void HandleEvent(JObject data) { string text = Extensions.Value<string>((IEnumerable<JToken>)data["eventType"]); if (text == "RecordStateChanged") { this.RecordStateChanged?.Invoke(data["eventData"].ToObject<RecordStateChangedEventData>()); } } private void HandleRequestResponse(JObject data) { if (!ulong.TryParse(Extensions.Value<string>((IEnumerable<JToken>)data["requestId"]), out var result)) { ObsSyncPlugin.Logger.LogWarning((object)"Received RequestResponse with non-integer request ID"); return; } if (!SentRequests.TryGetValue(result, out (ObsRequest, TaskCompletionSource<object>) value)) { ObsSyncPlugin.Logger.LogWarning((object)"Received RequestResponse with unknown request ID"); return; } ObsRequest item = value.Item1; TaskCompletionSource<object> item2 = value.Item2; string text = Extensions.Value<string>((IEnumerable<JToken>)data["requestType"]); if (text != item.RequestType) { ObsSyncPlugin.Logger.LogError((object)("Mismatched response type for request (" + item.RequestType + " != " + text + ")")); } else { ObsRequestResponse obsRequestResponse = item.MakeResponseObject(); obsRequestResponse.SetRequestResponseBody(data); item2.SetResult(obsRequestResponse); } } private string CreateAuthenticationData(string challenge, string salt) { if (string.IsNullOrEmpty(Password)) { throw new InvalidOperationException(); } SHA256 sHA = SHA256.Create(); string s = Password + salt; byte[] inArray = sHA.ComputeHash(Encoding.UTF8.GetBytes(s)); string text = Convert.ToBase64String(inArray); string s2 = text + challenge; byte[] inArray2 = sHA.ComputeHash(Encoding.UTF8.GetBytes(s2)); return Convert.ToBase64String(inArray2); } private void SendMessage(ObsMessage msg) { if (WebSocket.State == WebSocketState.Open) { string text = JsonConvert.SerializeObject((object)msg); byte[] bytes = Encoding.UTF8.GetBytes(text); ObsSyncPlugin.Logger.LogDebug((object)("Send: " + text)); WebSocket.SendAsync(bytes, WebSocketMessageType.Text, endOfMessage: true, default(CancellationToken)); } } private async void DisconnectInternal() { if (WebSocket.State == WebSocketState.Open && WebSocket.State == WebSocketState.CloseReceived) { await WebSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, "Client disconnecting", default(CancellationToken)); } } } public class StartRecordRequest : ObsRequest<ObsRequestResponse> { public StartRecordRequest() : base("StartRecord") { } } public class StopRecordRequest : ObsRequest<StopRecordResponse> { public StopRecordRequest() : base("StopRecord") { } } public class StopRecordResponse : ObsRequestResponse { public string OutputPath { get; private set; } = ""; protected override void SetResponseData(JObject data) { OutputPath = Extensions.Value<string>((IEnumerable<JToken>)data["outputPath"]); } } }