diff --git a/ACTOBSPlugin.cs b/ACTOBSPlugin.cs new file mode 100644 index 0000000..213a807 --- /dev/null +++ b/ACTOBSPlugin.cs @@ -0,0 +1,275 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text.RegularExpressions; +using System.Threading.Tasks; +using System.Windows.Forms; +using Advanced_Combat_Tracker; +using OBSWebsocketDotNet; +using OBSWebsocketDotNet.Communication; + +namespace ACTOBSPlugin +{ + public class ACTOBSPlugin : IActPluginV1 + { + private OBSWebsocket obs; + + private Label pluginStatusText; + private TabPage pluginScreenSpace; + private LogLineEventDelegate LogLineDel; + + private CombatToggleEventDelegate OnCombatEndDel; + + private PluginConfig config = new PluginConfig(); + + // We work around concurrency issues here by just using `ToList` on these collections whenever iterating over them + // Because changes to the structure are only initialized on init and on the UI thread, we don't need to lock + private List startRecordingRegexes = new List(); + private List stopRecordingRegexes = new List(); + private ConfigPanel configPanel; + private string lastVidFile; + + private string GetPluginDirectory() + { + var plugin = ActGlobals.oFormActMain.ActPlugins.Where(x => x.pluginObj == this).FirstOrDefault(); + if (plugin != null) + { + return Path.GetDirectoryName(plugin.pluginFile.FullName); + } + else + { + throw new Exception("Could not find ourselves in the plugin list!"); + } + } + + private void TryConnect() + { + if (config.Enabled) + { + try + { + obs.ConnectAsync(config.IPPort, config.Password); + } + catch (Exception ex) + { + ActGlobals.oFormActMain.BeginInvoke((MethodInvoker)delegate + { + MessageBox.Show("Connect failed : " + ex.Message, "Error", MessageBoxButtons.OK, MessageBoxIcon.Exclamation); + return; + }); + } + } + } + + private void Disconnect() + { + obs.Disconnect(); + } + + private void Obs_Disconnected(object sender, ObsDisconnectionInfo e) + { + UpdateStatus(); + Task.Delay(5000).ContinueWith(_ => { + TryConnect(); + }); + } + + private void Obs_Connected(object sender, EventArgs e) + { + UpdateStatus(); + } + + public void DeInitPlugin() + { + ActGlobals.oFormActMain.OnCombatEnd -= OnCombatEndDel; + ActGlobals.oFormActMain.BeforeLogLineRead -= LogLineDel; + config.Save(); + } + + private void RebuildRegexes(bool start, bool stop) + { + if (start) + { + startRecordingRegexes.Clear(); + foreach (var txtRegex in config.StartRecording) + { + try + { + startRecordingRegexes.Add(new Regex(txtRegex, RegexOptions.IgnoreCase | RegexOptions.Compiled)); + } + catch (Exception e) + { + ActGlobals.oFormActMain.WriteExceptionLog(e, "Exception parsing regex " + txtRegex); + } + } + } + if (stop) + { + stopRecordingRegexes.Clear(); + foreach (var txtRegex in config.StopRecording) + { + try + { + stopRecordingRegexes.Add(new Regex(txtRegex, RegexOptions.IgnoreCase | RegexOptions.Compiled)); + } + catch (Exception e) + { + ActGlobals.oFormActMain.WriteExceptionLog(e, "Exception parsing regex " + txtRegex); + } + } + } + } + + public void InitPlugin(TabPage pluginScreenSpace, Label pluginStatusText) + { + var dir = GetPluginDirectory(); + AppDomain.CurrentDomain.Load(File.ReadAllBytes(Path.Combine(dir, "System.Reactive.dll"))); + AppDomain.CurrentDomain.Load(File.ReadAllBytes(Path.Combine(dir, "System.Threading.Channels.dll"))); + AppDomain.CurrentDomain.Load(File.ReadAllBytes(Path.Combine(dir, "System.Threading.Tasks.Extensions.dll"))); + AppDomain.CurrentDomain.Load(File.ReadAllBytes(Path.Combine(dir, "Websocket.Client.dll"))); + AppDomain.CurrentDomain.Load(File.ReadAllBytes(Path.Combine(dir, "obs-websocket-dotnet.dll"))); + + this.pluginStatusText = pluginStatusText; + pluginStatusText.Text = "Loading PluginConfig"; + config.Load(); + RebuildRegexes(true, true); + config.ConfigChanged += (_, args) => { + RebuildRegexes(args.StartRecordingChanged, args.StopRecordingChanged); + if (args.EnabledChanged) + { + if (config.Enabled) + { + TryConnect(); + } + else + { + Disconnect(); + } + } + }; + + pluginStatusText.Text = "Creating ConfigPanel"; + this.pluginScreenSpace = pluginScreenSpace; + pluginScreenSpace.Text = "ACT OBS Plugin"; + configPanel = new ConfigPanel(config); + pluginScreenSpace.Controls.Add(configPanel); + pluginStatusText.Text = "In InitPlugin()"; + + PrivateInit(); + } + + private void PrivateInit() + { + obs = new OBSWebsocket(); + obs.Connected += Obs_Connected; + obs.Disconnected += Obs_Disconnected; + TryConnect(); + + LogLineDel = (bool isImport, LogLineEventArgs logInfo) => + { + try + { + var line = logInfo.originalLogLine; + if (obs.IsConnected) + { + if (!obs.GetRecordStatus().IsRecording) + { + foreach (var re in startRecordingRegexes) + { + if (re.IsMatch(line)) + { + obs.StartRecord(); + UpdateStatus(); + } + } + } + else + { + foreach (var re in stopRecordingRegexes) + { + if (re.IsMatch(line)) + { + // Get this info before calling `StopRecord` because it could change due to delay in stopping recording process + var currentEnc = ActGlobals.oFormActMain.ActiveZone.ActiveEncounter; + var currentZone = ActGlobals.oFormActMain.ActiveZone.ZoneName; + var vidFile = obs.StopRecord(); + if (config.AutoRename) + { + Task.Delay(5000).ContinueWith(_ => + { + var encTitle = currentEnc.Title; + var baseFilename = Path.GetFileNameWithoutExtension(vidFile); + var zoneEnc = string.Join("_", (currentZone + "_" + encTitle).Split(Path.GetInvalidFileNameChars(), StringSplitOptions.RemoveEmptyEntries)).TrimEnd('.'); + var extension = Path.GetExtension(vidFile); + var renamedFile = Path.Combine( + Path.GetDirectoryName(vidFile), + baseFilename + "_" + zoneEnc + extension + ); + File.Move(vidFile, renamedFile); + lastVidFile = renamedFile; + UpdateStatus(); + }); + } + else + { + UpdateStatus(); + } + } + } + } + } + } + catch (Exception ex) + { + Console.WriteLine("Exception: " + ex.ToString()); + } + }; + + OnCombatEndDel = (_, encounterInfo) => + { + return; + }; + + ActGlobals.oFormActMain.OnCombatEnd += OnCombatEndDel; + + ActGlobals.oFormActMain.BeforeLogLineRead += LogLineDel; + } + + private void UpdateStatus() + { + var status = ""; + + if (obs.IsConnected) + { + status += "Connected, "; + if (obs.GetRecordStatus().IsRecording) + { + status += "Recording"; + } + else + { + status += "Not Recording"; + } + } + else + { + status += "Disconnected, recording status unknown"; + } + + if (ActGlobals.oFormActMain.InvokeRequired) + { + ActGlobals.oFormActMain.Invoke((Action)(() => { + configPanel.SetStatus(status); + configPanel.SetLastFile(lastVidFile); + })); + } + else + { + configPanel.SetStatus(status); + configPanel.SetLastFile(lastVidFile); + } + } + } +} diff --git a/ACTOBSPlugin.csproj b/ACTOBSPlugin.csproj new file mode 100644 index 0000000..40cf01f --- /dev/null +++ b/ACTOBSPlugin.csproj @@ -0,0 +1,119 @@ + + + + + Debug + AnyCPU + {0AAE9AA1-B683-4461-9396-2AA0A819FEED} + Library + Properties + ACTOBSPlugin + ACTOBSPlugin + v4.8 + 512 + true + + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + true + bin\x64\Debug\ + DEBUG;TRACE + full + x64 + 7.3 + prompt + + + bin\x64\Release\ + TRACE + true + pdbonly + x64 + 7.3 + prompt + + + + ..\..\OverlayPlugin\Thirdparty\ACT\Advanced Combat Tracker.exe + False + + + packages\Newtonsoft.Json.13.0.1\lib\net45\Newtonsoft.Json.dll + False + + + packages\obs-websocket-dotnet.5.0.0.3\lib\netstandard2.0\obs-websocket-dotnet.dll + + + + + + packages\System.Reactive.4.3.2\lib\net46\System.Reactive.dll + + + packages\System.Runtime.CompilerServices.Unsafe.4.5.2\lib\netstandard2.0\System.Runtime.CompilerServices.Unsafe.dll + False + + + packages\System.Threading.Channels.4.7.0\lib\netstandard2.0\System.Threading.Channels.dll + + + packages\System.Threading.Tasks.Extensions.4.5.3\lib\netstandard2.0\System.Threading.Tasks.Extensions.dll + True + + + packages\System.ValueTuple.4.5.0\lib\net47\System.ValueTuple.dll + False + + + + + + + + + + + packages\Websocket.Client.4.4.43\lib\netstandard2.0\Websocket.Client.dll + + + + + + + UserControl + + + ConfigPanel.cs + + + + + + + + + + + ConfigPanel.cs + + + + \ No newline at end of file diff --git a/ACTOBSPlugin.sln b/ACTOBSPlugin.sln new file mode 100644 index 0000000..f0f9b9c --- /dev/null +++ b/ACTOBSPlugin.sln @@ -0,0 +1,25 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 16 +VisualStudioVersion = 16.0.32413.511 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ACTOBSPlugin", "ACTOBSPlugin.csproj", "{0AAE9AA1-B683-4461-9396-2AA0A819FEED}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|x64 = Debug|x64 + Release|x64 = Release|x64 + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {0AAE9AA1-B683-4461-9396-2AA0A819FEED}.Debug|x64.ActiveCfg = Debug|x64 + {0AAE9AA1-B683-4461-9396-2AA0A819FEED}.Debug|x64.Build.0 = Debug|x64 + {0AAE9AA1-B683-4461-9396-2AA0A819FEED}.Release|x64.ActiveCfg = Release|x64 + {0AAE9AA1-B683-4461-9396-2AA0A819FEED}.Release|x64.Build.0 = Release|x64 + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {A67BA73E-B5F3-4A8A-858E-2ECF88D155D4} + EndGlobalSection +EndGlobal diff --git a/ConfigPanel.Designer.cs b/ConfigPanel.Designer.cs new file mode 100644 index 0000000..a628244 --- /dev/null +++ b/ConfigPanel.Designer.cs @@ -0,0 +1,258 @@ + +namespace ACTOBSPlugin +{ + partial class ConfigPanel + { + /// + /// Required designer variable. + /// + private System.ComponentModel.IContainer components = null; + + /// + /// Clean up any resources being used. + /// + /// true if managed resources should be disposed; otherwise, false. + protected override void Dispose(bool disposing) + { + if (disposing && (components != null)) + { + components.Dispose(); + } + base.Dispose(disposing); + } + + #region Component Designer generated code + + /// + /// Required method for Designer support - do not modify + /// the contents of this method with the code editor. + /// + private void InitializeComponent() + { + this.chkEnabled = new System.Windows.Forms.CheckBox(); + this.chkAutoRename = new System.Windows.Forms.CheckBox(); + this.lblPassword = new System.Windows.Forms.Label(); + this.lblIPPort = new System.Windows.Forms.Label(); + this.txtPassword = new System.Windows.Forms.TextBox(); + this.txtIPPort = new System.Windows.Forms.TextBox(); + this.lblStatus = new System.Windows.Forms.Label(); + this.lblLastFile = new System.Windows.Forms.Label(); + this.lblInstructions = new System.Windows.Forms.Label(); + this.lblStartRecording = new System.Windows.Forms.Label(); + this.txtStartRecording = new System.Windows.Forms.TextBox(); + this.cmbStartRecording = new System.Windows.Forms.ComboBox(); + this.btnAddStartRecording = new System.Windows.Forms.Button(); + this.btnAddStopRecording = new System.Windows.Forms.Button(); + this.cmbStopRecording = new System.Windows.Forms.ComboBox(); + this.txtStopRecording = new System.Windows.Forms.TextBox(); + this.lblStopRecording = new System.Windows.Forms.Label(); + this.SuspendLayout(); + // + // chkEnabled + // + this.chkEnabled.AutoSize = true; + this.chkEnabled.Location = new System.Drawing.Point(4, 4); + this.chkEnabled.Name = "chkEnabled"; + this.chkEnabled.Size = new System.Drawing.Size(65, 17); + this.chkEnabled.TabIndex = 0; + this.chkEnabled.Text = "Enabled"; + this.chkEnabled.UseVisualStyleBackColor = true; + this.chkEnabled.CheckedChanged += new System.EventHandler(this.chkEnabled_CheckedChanged); + // + // chkAutoRename + // + this.chkAutoRename.AutoSize = true; + this.chkAutoRename.Location = new System.Drawing.Point(4, 28); + this.chkAutoRename.Name = "chkAutoRename"; + this.chkAutoRename.Size = new System.Drawing.Size(91, 17); + this.chkAutoRename.TabIndex = 1; + this.chkAutoRename.Text = "Auto Rename"; + this.chkAutoRename.UseVisualStyleBackColor = true; + this.chkAutoRename.CheckedChanged += new System.EventHandler(this.chkAutoRename_CheckedChanged); + // + // lblPassword + // + this.lblPassword.AutoSize = true; + this.lblPassword.Location = new System.Drawing.Point(101, 29); + this.lblPassword.Name = "lblPassword"; + this.lblPassword.Size = new System.Drawing.Size(53, 13); + this.lblPassword.TabIndex = 2; + this.lblPassword.Text = "Password"; + // + // lblIPPort + // + this.lblIPPort.AutoSize = true; + this.lblIPPort.Location = new System.Drawing.Point(101, 5); + this.lblIPPort.Name = "lblIPPort"; + this.lblIPPort.Size = new System.Drawing.Size(39, 13); + this.lblIPPort.TabIndex = 3; + this.lblIPPort.Text = "IP:Port"; + // + // txtPassword + // + this.txtPassword.Location = new System.Drawing.Point(160, 25); + this.txtPassword.Name = "txtPassword"; + this.txtPassword.Size = new System.Drawing.Size(100, 20); + this.txtPassword.TabIndex = 4; + this.txtPassword.UseSystemPasswordChar = true; + this.txtPassword.TextChanged += new System.EventHandler(this.txtPassword_TextChanged); + // + // txtIPPort + // + this.txtIPPort.Location = new System.Drawing.Point(160, 3); + this.txtIPPort.Name = "txtIPPort"; + this.txtIPPort.Size = new System.Drawing.Size(100, 20); + this.txtIPPort.TabIndex = 5; + this.txtIPPort.TextChanged += new System.EventHandler(this.txtIPPort_TextChanged); + // + // lblStatus + // + this.lblStatus.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left) + | System.Windows.Forms.AnchorStyles.Right))); + this.lblStatus.Location = new System.Drawing.Point(266, 6); + this.lblStatus.Name = "lblStatus"; + this.lblStatus.Size = new System.Drawing.Size(281, 17); + this.lblStatus.TabIndex = 6; + this.lblStatus.Text = "Status: "; + // + // lblLastFile + // + this.lblLastFile.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left) + | System.Windows.Forms.AnchorStyles.Right))); + this.lblLastFile.Location = new System.Drawing.Point(266, 28); + this.lblLastFile.Name = "lblLastFile"; + this.lblLastFile.Size = new System.Drawing.Size(281, 17); + this.lblLastFile.TabIndex = 7; + this.lblLastFile.Text = "Last File: "; + // + // lblInstructions + // + this.lblInstructions.AutoSize = true; + this.lblInstructions.Location = new System.Drawing.Point(4, 52); + this.lblInstructions.Name = "lblInstructions"; + this.lblInstructions.Size = new System.Drawing.Size(361, 26); + this.lblInstructions.TabIndex = 8; + this.lblInstructions.Text = "One regular expression per line. All regular expressions are case insensitive.\r\nD" + + "o not include leading and trailing `/` characters."; + // + // lblStartRecording + // + this.lblStartRecording.AutoSize = true; + this.lblStartRecording.Location = new System.Drawing.Point(7, 82); + this.lblStartRecording.Name = "lblStartRecording"; + this.lblStartRecording.Size = new System.Drawing.Size(81, 13); + this.lblStartRecording.TabIndex = 9; + this.lblStartRecording.Text = "Start Recording"; + // + // txtStartRecording + // + this.txtStartRecording.Location = new System.Drawing.Point(4, 99); + this.txtStartRecording.Multiline = true; + this.txtStartRecording.Name = "txtStartRecording"; + this.txtStartRecording.Size = new System.Drawing.Size(256, 421); + this.txtStartRecording.TabIndex = 10; + this.txtStartRecording.TextChanged += new System.EventHandler(this.txtStartRecording_TextChanged); + // + // cmbStartRecording + // + this.cmbStartRecording.FormattingEnabled = true; + this.cmbStartRecording.Location = new System.Drawing.Point(3, 526); + this.cmbStartRecording.Name = "cmbStartRecording"; + this.cmbStartRecording.Size = new System.Drawing.Size(228, 21); + this.cmbStartRecording.TabIndex = 11; + // + // btnAddStartRecording + // + this.btnAddStartRecording.Location = new System.Drawing.Point(237, 524); + this.btnAddStartRecording.Name = "btnAddStartRecording"; + this.btnAddStartRecording.Size = new System.Drawing.Size(23, 23); + this.btnAddStartRecording.TabIndex = 12; + this.btnAddStartRecording.Text = "+"; + this.btnAddStartRecording.UseVisualStyleBackColor = true; + this.btnAddStartRecording.Click += new System.EventHandler(this.btnAddStartRecording_Click); + // + // btnAddStopRecording + // + this.btnAddStopRecording.Location = new System.Drawing.Point(524, 524); + this.btnAddStopRecording.Name = "btnAddStopRecording"; + this.btnAddStopRecording.Size = new System.Drawing.Size(23, 23); + this.btnAddStopRecording.TabIndex = 14; + this.btnAddStopRecording.Text = "+"; + this.btnAddStopRecording.UseVisualStyleBackColor = true; + this.btnAddStopRecording.Click += new System.EventHandler(this.btnAddStopRecording_Click); + // + // cmbStopRecording + // + this.cmbStopRecording.FormattingEnabled = true; + this.cmbStopRecording.Location = new System.Drawing.Point(269, 526); + this.cmbStopRecording.Name = "cmbStopRecording"; + this.cmbStopRecording.Size = new System.Drawing.Size(249, 21); + this.cmbStopRecording.TabIndex = 13; + // + // txtStopRecording + // + this.txtStopRecording.Location = new System.Drawing.Point(269, 99); + this.txtStopRecording.Multiline = true; + this.txtStopRecording.Name = "txtStopRecording"; + this.txtStopRecording.Size = new System.Drawing.Size(278, 421); + this.txtStopRecording.TabIndex = 15; + this.txtStopRecording.TextChanged += new System.EventHandler(this.txtStopRecording_TextChanged); + // + // lblStopRecording + // + this.lblStopRecording.AutoSize = true; + this.lblStopRecording.Location = new System.Drawing.Point(266, 82); + this.lblStopRecording.Name = "lblStopRecording"; + this.lblStopRecording.Size = new System.Drawing.Size(81, 13); + this.lblStopRecording.TabIndex = 16; + this.lblStopRecording.Text = "Stop Recording"; + // + // ConfigPanel + // + this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F); + this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; + this.Controls.Add(this.lblStopRecording); + this.Controls.Add(this.txtStopRecording); + this.Controls.Add(this.btnAddStopRecording); + this.Controls.Add(this.cmbStopRecording); + this.Controls.Add(this.btnAddStartRecording); + this.Controls.Add(this.cmbStartRecording); + this.Controls.Add(this.txtStartRecording); + this.Controls.Add(this.lblStartRecording); + this.Controls.Add(this.lblInstructions); + this.Controls.Add(this.lblLastFile); + this.Controls.Add(this.lblStatus); + this.Controls.Add(this.txtIPPort); + this.Controls.Add(this.txtPassword); + this.Controls.Add(this.lblIPPort); + this.Controls.Add(this.lblPassword); + this.Controls.Add(this.chkAutoRename); + this.Controls.Add(this.chkEnabled); + this.Name = "ConfigPanel"; + this.Size = new System.Drawing.Size(550, 550); + this.ResumeLayout(false); + this.PerformLayout(); + + } + + #endregion + + private System.Windows.Forms.CheckBox chkEnabled; + private System.Windows.Forms.CheckBox chkAutoRename; + private System.Windows.Forms.Label lblPassword; + private System.Windows.Forms.Label lblIPPort; + private System.Windows.Forms.TextBox txtPassword; + private System.Windows.Forms.TextBox txtIPPort; + private System.Windows.Forms.Label lblStatus; + private System.Windows.Forms.Label lblLastFile; + private System.Windows.Forms.Label lblInstructions; + private System.Windows.Forms.Label lblStartRecording; + private System.Windows.Forms.TextBox txtStartRecording; + private System.Windows.Forms.ComboBox cmbStartRecording; + private System.Windows.Forms.Button btnAddStartRecording; + private System.Windows.Forms.Button btnAddStopRecording; + private System.Windows.Forms.ComboBox cmbStopRecording; + private System.Windows.Forms.TextBox txtStopRecording; + private System.Windows.Forms.Label lblStopRecording; + } +} diff --git a/ConfigPanel.cs b/ConfigPanel.cs new file mode 100644 index 0000000..509a0bc --- /dev/null +++ b/ConfigPanel.cs @@ -0,0 +1,120 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Data; +using System.Drawing; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Windows.Forms; + +namespace ACTOBSPlugin +{ + public partial class ConfigPanel : UserControl + { + private PluginConfig config; + + private Dictionary startRecordingRegexes = new Dictionary() { + // FFXIV + { "FFXIV - ACT Combat Start", @"^(?# FFXIV - ACT Combat Start)(?260)\|(?[^|]*)\|(?1)\|" }, + { "FFXIV - Game Combat Start", @"^(?# FFXIV - Game Combat Start)(?260)\|(?[^|]*)\|(?[^|]*)\|(?1)\|" }, + { "FFXIV - Countdown Start", @"(?# FFXIV - Countdown Start)\|Battle commencing in (?