using 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"]);
}
}
}