All 5 phases of the open-source Decal rebuild: Phase 1: 14 decompiled .NET projects (Interop.*, Adapter, FileService, DecalUtil) Phase 2: 10 native DLLs rewritten as C# COM servers with matching GUIDs - DecalDat, DHS, SpellFilter, DecalInput, DecalNet, DecalFilters - Decal.Core, DecalControls, DecalRender, D3DService Phase 3: C++ shims for Inject.DLL (D3D9 hooking) and LauncherHook.DLL Phase 4: DenAgent WinForms tray application Phase 5: WiX installer and build script 25 C# projects building with 0 errors. Native C++ projects require VS 2022 + Windows SDK (x86). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
408 lines
15 KiB
C#
408 lines
15 KiB
C#
using System;
|
|
using System.Diagnostics;
|
|
using System.IO;
|
|
using System.Windows.Forms;
|
|
using Microsoft.Win32;
|
|
|
|
namespace Decal.DenAgent
|
|
{
|
|
/// <summary>
|
|
/// Main plugin management dialog. Shows all registered Decal plugins/services
|
|
/// with enable/disable toggle, drag-drop reordering, and version display.
|
|
/// Replaces the MFC CDenAgentDlg class.
|
|
/// </summary>
|
|
internal sealed class AgentForm : Form
|
|
{
|
|
private readonly ListView _pluginList;
|
|
private readonly ImageList _imageList;
|
|
private readonly Button _btnClose;
|
|
private readonly Button _btnAdd;
|
|
private readonly Button _btnRemove;
|
|
private readonly Button _btnRefresh;
|
|
private readonly Button _btnOptions;
|
|
private readonly Label _lblMessages;
|
|
private readonly Label _lblMemLocs;
|
|
|
|
// Plugin group definitions matching the original DenAgent
|
|
private static readonly PluginGroup[] Groups =
|
|
{
|
|
new("Plugins", "Plugins", 0),
|
|
new("Network Filters", "NetworkFilters", 1),
|
|
new("File Filters", "FileFilters", 1),
|
|
new("Services", "Services", 2),
|
|
new("Surrogates", "Surrogates", 3),
|
|
new("Input Actions", "InputActions", 4),
|
|
};
|
|
|
|
public AgentForm()
|
|
{
|
|
Text = "Decal Agent " + GetAgentVersion();
|
|
Size = new System.Drawing.Size(420, 360);
|
|
FormBorderStyle = FormBorderStyle.FixedDialog;
|
|
MaximizeBox = false;
|
|
StartPosition = FormStartPosition.CenterScreen;
|
|
|
|
// Image list for enabled/disabled/group icons
|
|
_imageList = new ImageList { ImageSize = new System.Drawing.Size(16, 16) };
|
|
// Index 0 = group (folder), 1 = enabled (check), 2 = disabled (x)
|
|
_imageList.Images.Add(CreateColorIcon(System.Drawing.Color.SteelBlue)); // 0: group
|
|
_imageList.Images.Add(CreateColorIcon(System.Drawing.Color.ForestGreen)); // 1: enabled
|
|
_imageList.Images.Add(CreateColorIcon(System.Drawing.Color.Gray)); // 2: disabled
|
|
|
|
// Plugin list view
|
|
_pluginList = new ListView
|
|
{
|
|
Location = new System.Drawing.Point(10, 35),
|
|
Size = new System.Drawing.Size(295, 230),
|
|
View = View.Details,
|
|
FullRowSelect = true,
|
|
SmallImageList = _imageList,
|
|
HeaderStyle = ColumnHeaderStyle.None,
|
|
AllowDrop = true
|
|
};
|
|
_pluginList.Columns.Add("Component", 210);
|
|
_pluginList.Columns.Add("Version", 80, HorizontalAlignment.Right);
|
|
_pluginList.MouseClick += OnPluginListClick;
|
|
Controls.Add(_pluginList);
|
|
|
|
// Description label
|
|
var lblDesc = new Label
|
|
{
|
|
Text = "Choose the Plugins you'd like to run.",
|
|
Location = new System.Drawing.Point(10, 10),
|
|
Size = new System.Drawing.Size(390, 20)
|
|
};
|
|
Controls.Add(lblDesc);
|
|
|
|
// Buttons
|
|
int btnX = 320;
|
|
_btnClose = AddButton("Close", btnX, 300, (s, e) => Close());
|
|
_btnRefresh = AddButton("Refresh", btnX, 270, (s, e) => LoadPluginList());
|
|
_btnAdd = AddButton("Add", btnX, 35, (s, e) => OnAddPlugin());
|
|
_btnRemove = AddButton("Remove", btnX, 65, (s, e) => OnRemovePlugin());
|
|
_btnOptions = AddButton("Options", btnX, 105, (s, e) => OnShowOptions());
|
|
|
|
// Status labels
|
|
_lblMessages = new Label
|
|
{
|
|
Text = "Messages: -",
|
|
Location = new System.Drawing.Point(10, 275),
|
|
Size = new System.Drawing.Size(300, 16),
|
|
Font = new System.Drawing.Font("Segoe UI", 8f)
|
|
};
|
|
Controls.Add(_lblMessages);
|
|
|
|
_lblMemLocs = new Label
|
|
{
|
|
Text = "Memory Locations: -",
|
|
Location = new System.Drawing.Point(10, 293),
|
|
Size = new System.Drawing.Size(300, 16),
|
|
Font = new System.Drawing.Font("Segoe UI", 8f)
|
|
};
|
|
Controls.Add(_lblMemLocs);
|
|
|
|
AcceptButton = _btnClose;
|
|
|
|
LoadPluginList();
|
|
UpdateFileInfo();
|
|
}
|
|
|
|
private Button AddButton(string text, int x, int y, EventHandler click)
|
|
{
|
|
var btn = new Button
|
|
{
|
|
Text = text,
|
|
Location = new System.Drawing.Point(x, y),
|
|
Size = new System.Drawing.Size(80, 25)
|
|
};
|
|
btn.Click += click;
|
|
Controls.Add(btn);
|
|
return btn;
|
|
}
|
|
|
|
private void LoadPluginList()
|
|
{
|
|
_pluginList.BeginUpdate();
|
|
_pluginList.Items.Clear();
|
|
|
|
foreach (var group in Groups)
|
|
{
|
|
// Add group header
|
|
var groupItem = new ListViewItem(group.DisplayName, 0)
|
|
{
|
|
Tag = null // Group headers have no tag
|
|
};
|
|
groupItem.SubItems.Add("");
|
|
_pluginList.Items.Add(groupItem);
|
|
|
|
// Enumerate plugins in this group from registry
|
|
string keyPath = $@"SOFTWARE\Decal\{group.RegistryKey}";
|
|
try
|
|
{
|
|
using var key = Registry.LocalMachine.OpenSubKey(keyPath);
|
|
if (key == null) continue;
|
|
|
|
foreach (string clsid in key.GetSubKeyNames())
|
|
{
|
|
using var pluginKey = key.OpenSubKey(clsid);
|
|
if (pluginKey == null) continue;
|
|
|
|
string name = pluginKey.GetValue("FriendlyName") as string
|
|
?? pluginKey.GetValue("") as string
|
|
?? clsid;
|
|
|
|
bool enabled = true;
|
|
object enabledVal = pluginKey.GetValue("Enabled");
|
|
if (enabledVal is int ei)
|
|
enabled = ei != 0;
|
|
|
|
var info = new PluginInfo(clsid, group.RegistryKey, name, enabled);
|
|
int imageIdx = enabled ? 1 : 2;
|
|
|
|
var item = new ListViewItem(" " + name, imageIdx)
|
|
{
|
|
Tag = info,
|
|
IndentCount = 1
|
|
};
|
|
|
|
// Get version from COM registration
|
|
string version = GetPluginVersion(clsid);
|
|
item.SubItems.Add(version);
|
|
|
|
_pluginList.Items.Add(item);
|
|
}
|
|
}
|
|
catch { /* Skip groups we can't read */ }
|
|
}
|
|
|
|
_pluginList.EndUpdate();
|
|
}
|
|
|
|
private void OnPluginListClick(object sender, MouseEventArgs e)
|
|
{
|
|
var hitTest = _pluginList.HitTest(e.Location);
|
|
if (hitTest.Item == null) return;
|
|
|
|
var info = hitTest.Item.Tag as PluginInfo;
|
|
if (info == null) return; // Group header
|
|
|
|
// Toggle enabled state when clicking the icon area
|
|
if (e.X < 40)
|
|
{
|
|
info.Enabled = !info.Enabled;
|
|
hitTest.Item.ImageIndex = info.Enabled ? 1 : 2;
|
|
|
|
// Save to registry
|
|
try
|
|
{
|
|
string keyPath = $@"SOFTWARE\Decal\{info.GroupKey}\{info.Clsid}";
|
|
using var key = Registry.LocalMachine.OpenSubKey(keyPath, writable: true);
|
|
key?.SetValue("Enabled", info.Enabled ? 1 : 0, RegistryValueKind.DWord);
|
|
}
|
|
catch { /* May need admin rights */ }
|
|
}
|
|
}
|
|
|
|
private void OnAddPlugin()
|
|
{
|
|
using var dlg = new OpenFileDialog
|
|
{
|
|
Title = "Select Plugin DLL",
|
|
Filter = "DLL files (*.dll)|*.dll|All files (*.*)|*.*",
|
|
CheckFileExists = true
|
|
};
|
|
|
|
if (dlg.ShowDialog(this) != DialogResult.OK)
|
|
return;
|
|
|
|
// Register the DLL as a COM server and add to Decal registry
|
|
string dllPath = dlg.FileName;
|
|
try
|
|
{
|
|
var psi = new ProcessStartInfo
|
|
{
|
|
FileName = "regsvr32.exe",
|
|
Arguments = $"/s \"{dllPath}\"",
|
|
UseShellExecute = true,
|
|
Verb = "runas"
|
|
};
|
|
Process.Start(psi)?.WaitForExit();
|
|
LoadPluginList();
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
MessageBox.Show($"Failed to register plugin: {ex.Message}", "Error",
|
|
MessageBoxButtons.OK, MessageBoxIcon.Error);
|
|
}
|
|
}
|
|
|
|
private void OnRemovePlugin()
|
|
{
|
|
if (_pluginList.SelectedItems.Count == 0) return;
|
|
var item = _pluginList.SelectedItems[0];
|
|
var info = item.Tag as PluginInfo;
|
|
if (info == null) return;
|
|
|
|
var result = MessageBox.Show(
|
|
$"Remove plugin '{info.Name}'?\nThis will unregister it from Decal.",
|
|
"Remove Plugin",
|
|
MessageBoxButtons.OKCancel, MessageBoxIcon.Warning);
|
|
|
|
if (result != DialogResult.OK) return;
|
|
|
|
try
|
|
{
|
|
// Check for uninstaller
|
|
string keyPath = $@"SOFTWARE\Decal\{info.GroupKey}\{info.Clsid}";
|
|
using var key = Registry.LocalMachine.OpenSubKey(keyPath);
|
|
string uninstaller = key?.GetValue("Uninstaller") as string;
|
|
|
|
if (!string.IsNullOrEmpty(uninstaller))
|
|
{
|
|
Process.Start(uninstaller);
|
|
}
|
|
else
|
|
{
|
|
// Just delete the registry entry
|
|
string parentPath = $@"SOFTWARE\Decal\{info.GroupKey}";
|
|
using var parentKey = Registry.LocalMachine.OpenSubKey(parentPath, writable: true);
|
|
parentKey?.DeleteSubKeyTree(info.Clsid, false);
|
|
}
|
|
|
|
LoadPluginList();
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
MessageBox.Show($"Failed to remove plugin: {ex.Message}", "Error",
|
|
MessageBoxButtons.OK, MessageBoxIcon.Error);
|
|
}
|
|
}
|
|
|
|
private void OnShowOptions()
|
|
{
|
|
using var dlg = new OptionsForm();
|
|
dlg.ShowDialog(this);
|
|
}
|
|
|
|
private static string GetPluginVersion(string clsid)
|
|
{
|
|
try
|
|
{
|
|
string keyPath = $@"CLSID\{clsid}\InprocServer32";
|
|
using var key = Registry.ClassesRoot.OpenSubKey(keyPath);
|
|
if (key == null) return "<Not registered>";
|
|
|
|
string dllPath = key.GetValue("") as string ?? "";
|
|
|
|
// .NET assemblies register as mscoree.dll; get the CodeBase instead
|
|
if (dllPath.EndsWith("mscoree.dll", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
string codeBase = key.GetValue("CodeBase") as string ?? "";
|
|
if (codeBase.StartsWith("file:///", StringComparison.OrdinalIgnoreCase))
|
|
dllPath = codeBase.Substring(8).Replace('/', '\\');
|
|
}
|
|
|
|
if (!File.Exists(dllPath))
|
|
return "<No DLL>";
|
|
|
|
var vi = FileVersionInfo.GetVersionInfo(dllPath);
|
|
return vi.FileVersion ?? "<No Version>";
|
|
}
|
|
catch
|
|
{
|
|
return "<Error>";
|
|
}
|
|
}
|
|
|
|
private void UpdateFileInfo()
|
|
{
|
|
// Check for XML data files used by Decal
|
|
string agentPath = GetAgentPath();
|
|
if (string.IsNullOrEmpty(agentPath)) return;
|
|
|
|
string messagesFile = Path.Combine(agentPath, "messages.xml");
|
|
string memlocsFile = Path.Combine(agentPath, "memlocs.xml");
|
|
|
|
_lblMessages.Text = "Messages: " + (File.Exists(messagesFile)
|
|
? File.GetLastWriteTime(messagesFile).ToString("yyyy-MM-dd HH:mm")
|
|
: "Not found");
|
|
|
|
_lblMemLocs.Text = "Memory Locations: " + (File.Exists(memlocsFile)
|
|
? File.GetLastWriteTime(memlocsFile).ToString("yyyy-MM-dd HH:mm")
|
|
: "Not found");
|
|
}
|
|
|
|
private static string GetAgentPath()
|
|
{
|
|
try
|
|
{
|
|
using var key = Registry.LocalMachine.OpenSubKey(@"SOFTWARE\Decal\Agent");
|
|
return key?.GetValue("AgentPath") as string ?? "";
|
|
}
|
|
catch { return ""; }
|
|
}
|
|
|
|
private static string GetAgentVersion()
|
|
{
|
|
try
|
|
{
|
|
var vi = FileVersionInfo.GetVersionInfo(
|
|
System.Reflection.Assembly.GetExecutingAssembly().Location);
|
|
return vi.FileVersion ?? "0.0.0.0";
|
|
}
|
|
catch { return "0.0.0.0"; }
|
|
}
|
|
|
|
private static System.Drawing.Icon CreateColorIcon(System.Drawing.Color color)
|
|
{
|
|
using var bmp = new System.Drawing.Bitmap(16, 16);
|
|
using var g = System.Drawing.Graphics.FromImage(bmp);
|
|
g.Clear(System.Drawing.Color.Transparent);
|
|
using var brush = new System.Drawing.SolidBrush(color);
|
|
g.FillEllipse(brush, 2, 2, 12, 12);
|
|
return System.Drawing.Icon.FromHandle(bmp.GetHicon());
|
|
}
|
|
|
|
protected override void OnFormClosing(FormClosingEventArgs e)
|
|
{
|
|
if (e.CloseReason == CloseReason.UserClosing)
|
|
{
|
|
e.Cancel = true;
|
|
Hide();
|
|
}
|
|
else
|
|
{
|
|
base.OnFormClosing(e);
|
|
}
|
|
}
|
|
|
|
private class PluginGroup
|
|
{
|
|
public string DisplayName { get; }
|
|
public string RegistryKey { get; }
|
|
public int IconIndex { get; }
|
|
public PluginGroup(string displayName, string registryKey, int iconIndex)
|
|
{
|
|
DisplayName = displayName;
|
|
RegistryKey = registryKey;
|
|
IconIndex = iconIndex;
|
|
}
|
|
}
|
|
|
|
private class PluginInfo
|
|
{
|
|
public string Clsid { get; }
|
|
public string GroupKey { get; }
|
|
public string Name { get; }
|
|
public bool Enabled { get; set; }
|
|
|
|
public PluginInfo(string clsid, string groupKey, string name, bool enabled)
|
|
{
|
|
Clsid = clsid;
|
|
GroupKey = groupKey;
|
|
Name = name;
|
|
Enabled = enabled;
|
|
}
|
|
}
|
|
}
|
|
}
|