Initial commit: Complete open-source Decal rebuild
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>
This commit is contained in:
commit
d1442e3747
1382 changed files with 170725 additions and 0 deletions
408
Managed/Decal.DenAgent/AgentForm.cs
Normal file
408
Managed/Decal.DenAgent/AgentForm.cs
Normal file
|
|
@ -0,0 +1,408 @@
|
|||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
10
Managed/Decal.DenAgent/Decal.DenAgent.csproj
Normal file
10
Managed/Decal.DenAgent/Decal.DenAgent.csproj
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<OutputType>WinExe</OutputType>
|
||||
<UseWindowsForms>true</UseWindowsForms>
|
||||
<!-- <ApplicationIcon>decal.ico</ApplicationIcon> -->
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Decal.Interop.Core\Decal.Interop.Core.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
226
Managed/Decal.DenAgent/OptionsForm.cs
Normal file
226
Managed/Decal.DenAgent/OptionsForm.cs
Normal file
|
|
@ -0,0 +1,226 @@
|
|||
using System;
|
||||
using System.Windows.Forms;
|
||||
using Microsoft.Win32;
|
||||
|
||||
namespace Decal.DenAgent
|
||||
{
|
||||
/// <summary>
|
||||
/// Options/settings dialog for Decal configuration.
|
||||
/// Manages registry settings under HKLM\SOFTWARE\Decal.
|
||||
/// Replaces the MFC cOptionsDlg class.
|
||||
/// </summary>
|
||||
internal sealed class OptionsForm : Form
|
||||
{
|
||||
private readonly NumericUpDown _nudBarAlpha;
|
||||
private readonly NumericUpDown _nudViewAlpha;
|
||||
private readonly ComboBox _cbDockPos;
|
||||
private readonly CheckBox _chkTimestamp;
|
||||
private readonly TextBox _txtTimestampFormat;
|
||||
private readonly CheckBox _chkWindowed;
|
||||
private readonly CheckBox _chkDualLog;
|
||||
private readonly CheckBox _chkNoMovies;
|
||||
private readonly CheckBox _chkNoSplash;
|
||||
|
||||
public OptionsForm()
|
||||
{
|
||||
Text = "Decal Options";
|
||||
Size = new System.Drawing.Size(380, 340);
|
||||
FormBorderStyle = FormBorderStyle.FixedDialog;
|
||||
MaximizeBox = false;
|
||||
MinimizeBox = false;
|
||||
StartPosition = FormStartPosition.CenterParent;
|
||||
|
||||
int y = 15;
|
||||
|
||||
// View Options group
|
||||
var grpView = new GroupBox
|
||||
{
|
||||
Text = "View Options",
|
||||
Location = new System.Drawing.Point(10, y),
|
||||
Size = new System.Drawing.Size(350, 90)
|
||||
};
|
||||
Controls.Add(grpView);
|
||||
|
||||
grpView.Controls.Add(new Label { Text = "Bar Alpha:", Location = new System.Drawing.Point(10, 25), AutoSize = true });
|
||||
_nudBarAlpha = new NumericUpDown
|
||||
{
|
||||
Location = new System.Drawing.Point(100, 22),
|
||||
Size = new System.Drawing.Size(60, 23),
|
||||
Minimum = 0, Maximum = 255, Value = 255
|
||||
};
|
||||
grpView.Controls.Add(_nudBarAlpha);
|
||||
|
||||
grpView.Controls.Add(new Label { Text = "View Alpha:", Location = new System.Drawing.Point(180, 25), AutoSize = true });
|
||||
_nudViewAlpha = new NumericUpDown
|
||||
{
|
||||
Location = new System.Drawing.Point(270, 22),
|
||||
Size = new System.Drawing.Size(60, 23),
|
||||
Minimum = 0, Maximum = 255, Value = 255
|
||||
};
|
||||
grpView.Controls.Add(_nudViewAlpha);
|
||||
|
||||
grpView.Controls.Add(new Label { Text = "Dock Position:", Location = new System.Drawing.Point(10, 58), AutoSize = true });
|
||||
_cbDockPos = new ComboBox
|
||||
{
|
||||
Location = new System.Drawing.Point(100, 55),
|
||||
Size = new System.Drawing.Size(100, 23),
|
||||
DropDownStyle = ComboBoxStyle.DropDownList
|
||||
};
|
||||
_cbDockPos.Items.AddRange(new object[] { "Top", "Left", "Right" });
|
||||
_cbDockPos.SelectedIndex = 0;
|
||||
grpView.Controls.Add(_cbDockPos);
|
||||
|
||||
y += 100;
|
||||
|
||||
// Timestamp group
|
||||
var grpTS = new GroupBox
|
||||
{
|
||||
Text = "Time Stamping",
|
||||
Location = new System.Drawing.Point(10, y),
|
||||
Size = new System.Drawing.Size(350, 55)
|
||||
};
|
||||
Controls.Add(grpTS);
|
||||
|
||||
_chkTimestamp = new CheckBox
|
||||
{
|
||||
Text = "Enable Timestamps",
|
||||
Location = new System.Drawing.Point(10, 22),
|
||||
AutoSize = true
|
||||
};
|
||||
_chkTimestamp.CheckedChanged += (s, e) => _txtTimestampFormat.Enabled = _chkTimestamp.Checked;
|
||||
grpTS.Controls.Add(_chkTimestamp);
|
||||
|
||||
grpTS.Controls.Add(new Label { Text = "Format:", Location = new System.Drawing.Point(170, 24), AutoSize = true });
|
||||
_txtTimestampFormat = new TextBox
|
||||
{
|
||||
Location = new System.Drawing.Point(220, 21),
|
||||
Size = new System.Drawing.Size(110, 23),
|
||||
Text = "[%H:%M]",
|
||||
Enabled = false
|
||||
};
|
||||
grpTS.Controls.Add(_txtTimestampFormat);
|
||||
|
||||
y += 65;
|
||||
|
||||
// Client Patches group
|
||||
var grpPatches = new GroupBox
|
||||
{
|
||||
Text = "Client Patches",
|
||||
Location = new System.Drawing.Point(10, y),
|
||||
Size = new System.Drawing.Size(350, 90)
|
||||
};
|
||||
Controls.Add(grpPatches);
|
||||
|
||||
_chkWindowed = new CheckBox
|
||||
{
|
||||
Text = "Allow Windowed Mode",
|
||||
Location = new System.Drawing.Point(10, 20),
|
||||
AutoSize = true
|
||||
};
|
||||
grpPatches.Controls.Add(_chkWindowed);
|
||||
|
||||
_chkDualLog = new CheckBox
|
||||
{
|
||||
Text = "Allow Dual Logging",
|
||||
Location = new System.Drawing.Point(10, 42),
|
||||
AutoSize = true
|
||||
};
|
||||
grpPatches.Controls.Add(_chkDualLog);
|
||||
|
||||
_chkNoMovies = new CheckBox
|
||||
{
|
||||
Text = "Disable Movies",
|
||||
Location = new System.Drawing.Point(190, 20),
|
||||
AutoSize = true
|
||||
};
|
||||
grpPatches.Controls.Add(_chkNoMovies);
|
||||
|
||||
_chkNoSplash = new CheckBox
|
||||
{
|
||||
Text = "Disable Splash Screen",
|
||||
Location = new System.Drawing.Point(190, 42),
|
||||
AutoSize = true
|
||||
};
|
||||
grpPatches.Controls.Add(_chkNoSplash);
|
||||
|
||||
y += 100;
|
||||
|
||||
// OK / Cancel buttons
|
||||
var btnOK = new Button
|
||||
{
|
||||
Text = "OK",
|
||||
DialogResult = DialogResult.OK,
|
||||
Location = new System.Drawing.Point(200, y),
|
||||
Size = new System.Drawing.Size(75, 25)
|
||||
};
|
||||
btnOK.Click += OnOK;
|
||||
Controls.Add(btnOK);
|
||||
|
||||
var btnCancel = new Button
|
||||
{
|
||||
Text = "Cancel",
|
||||
DialogResult = DialogResult.Cancel,
|
||||
Location = new System.Drawing.Point(285, y),
|
||||
Size = new System.Drawing.Size(75, 25)
|
||||
};
|
||||
Controls.Add(btnCancel);
|
||||
|
||||
AcceptButton = btnOK;
|
||||
CancelButton = btnCancel;
|
||||
|
||||
LoadSettings();
|
||||
}
|
||||
|
||||
private void LoadSettings()
|
||||
{
|
||||
try
|
||||
{
|
||||
using var key = Registry.LocalMachine.OpenSubKey(@"SOFTWARE\Decal");
|
||||
if (key == null) return;
|
||||
|
||||
_nudBarAlpha.Value = Math.Min(255, Math.Max(0, (int)(key.GetValue("BarAlpha") ?? 255)));
|
||||
_nudViewAlpha.Value = Math.Min(255, Math.Max(0, (int)(key.GetValue("ViewAlpha") ?? 255)));
|
||||
|
||||
int dockPos = (int)(key.GetValue("BarDock") ?? 0);
|
||||
if (dockPos >= 0 && dockPos < _cbDockPos.Items.Count)
|
||||
_cbDockPos.SelectedIndex = dockPos;
|
||||
|
||||
_chkTimestamp.Checked = ((int)(key.GetValue("Timestamp") ?? 0)) != 0;
|
||||
_txtTimestampFormat.Text = key.GetValue("TimestampFormat") as string ?? "[%H:%M]";
|
||||
_txtTimestampFormat.Enabled = _chkTimestamp.Checked;
|
||||
|
||||
_chkWindowed.Checked = ((int)(key.GetValue("AllowWindowed") ?? 0)) != 0;
|
||||
_chkDualLog.Checked = ((int)(key.GetValue("AllowDualLog") ?? 0)) != 0;
|
||||
_chkNoMovies.Checked = ((int)(key.GetValue("NoMovies") ?? 0)) != 0;
|
||||
_chkNoSplash.Checked = ((int)(key.GetValue("NoSplash") ?? 0)) != 0;
|
||||
}
|
||||
catch { /* Default values are fine */ }
|
||||
}
|
||||
|
||||
private void OnOK(object sender, EventArgs e)
|
||||
{
|
||||
try
|
||||
{
|
||||
using var key = Registry.LocalMachine.CreateSubKey(@"SOFTWARE\Decal");
|
||||
if (key == null) return;
|
||||
|
||||
key.SetValue("BarAlpha", (int)_nudBarAlpha.Value, RegistryValueKind.DWord);
|
||||
key.SetValue("ViewAlpha", (int)_nudViewAlpha.Value, RegistryValueKind.DWord);
|
||||
key.SetValue("BarDock", _cbDockPos.SelectedIndex, RegistryValueKind.DWord);
|
||||
|
||||
key.SetValue("Timestamp", _chkTimestamp.Checked ? 1 : 0, RegistryValueKind.DWord);
|
||||
key.SetValue("TimestampFormat", _txtTimestampFormat.Text);
|
||||
|
||||
key.SetValue("AllowWindowed", _chkWindowed.Checked ? 1 : 0, RegistryValueKind.DWord);
|
||||
key.SetValue("AllowDualLog", _chkDualLog.Checked ? 1 : 0, RegistryValueKind.DWord);
|
||||
key.SetValue("NoMovies", _chkNoMovies.Checked ? 1 : 0, RegistryValueKind.DWord);
|
||||
key.SetValue("NoSplash", _chkNoSplash.Checked ? 1 : 0, RegistryValueKind.DWord);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
MessageBox.Show($"Failed to save settings: {ex.Message}\n\nYou may need to run as Administrator.",
|
||||
"Error", MessageBoxButtons.OK, MessageBoxIcon.Error);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
26
Managed/Decal.DenAgent/Program.cs
Normal file
26
Managed/Decal.DenAgent/Program.cs
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
using System;
|
||||
using System.Threading;
|
||||
using System.Windows.Forms;
|
||||
|
||||
namespace Decal.DenAgent
|
||||
{
|
||||
static class Program
|
||||
{
|
||||
[STAThread]
|
||||
static void Main()
|
||||
{
|
||||
// Single instance check
|
||||
using var mutex = new Mutex(true, "DecalAgentMutex", out bool createdNew);
|
||||
if (!createdNew)
|
||||
{
|
||||
MessageBox.Show("Decal Agent is already running.", "Decal Agent",
|
||||
MessageBoxButtons.OK, MessageBoxIcon.Information);
|
||||
return;
|
||||
}
|
||||
|
||||
Application.EnableVisualStyles();
|
||||
Application.SetCompatibleTextRenderingDefault(false);
|
||||
Application.Run(new TrayContext());
|
||||
}
|
||||
}
|
||||
}
|
||||
265
Managed/Decal.DenAgent/TrayContext.cs
Normal file
265
Managed/Decal.DenAgent/TrayContext.cs
Normal file
|
|
@ -0,0 +1,265 @@
|
|||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Windows.Forms;
|
||||
using Microsoft.Win32;
|
||||
|
||||
namespace Decal.DenAgent
|
||||
{
|
||||
/// <summary>
|
||||
/// Application context that manages the system tray icon and lobby injection timer.
|
||||
/// Replaces the MFC CTrayWnd class.
|
||||
/// </summary>
|
||||
internal sealed class TrayContext : ApplicationContext
|
||||
{
|
||||
private readonly NotifyIcon _trayIcon;
|
||||
private readonly Timer _lobbyTimer;
|
||||
private AgentForm _agentForm;
|
||||
|
||||
// P/Invoke for window enumeration and DLL injection
|
||||
[DllImport("user32.dll")]
|
||||
private static extern bool EnumWindows(EnumWindowsProc lpEnumFunc, IntPtr lParam);
|
||||
|
||||
[DllImport("user32.dll", CharSet = CharSet.Auto)]
|
||||
private static extern int GetClassName(IntPtr hWnd, char[] lpClassName, int nMaxCount);
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
private static extern uint GetWindowThreadProcessId(IntPtr hWnd, out uint lpdwProcessId);
|
||||
|
||||
[DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
|
||||
private static extern IntPtr CreateSemaphore(IntPtr lpAttributes, int lInitialCount,
|
||||
int lMaximumCount, string lpName);
|
||||
|
||||
[DllImport("kernel32.dll")]
|
||||
private static extern bool CloseHandle(IntPtr hObject);
|
||||
|
||||
// LauncherHook P/Invoke
|
||||
[DllImport("LauncherHook.dll", CallingConvention = CallingConvention.StdCall)]
|
||||
private static extern void LauncherHookEnable();
|
||||
|
||||
[DllImport("LauncherHook.dll", CallingConvention = CallingConvention.StdCall)]
|
||||
private static extern void LauncherHookDisable();
|
||||
|
||||
// Inject.DLL injection via CreateRemoteThread
|
||||
[DllImport("kernel32.dll", SetLastError = true)]
|
||||
private static extern IntPtr OpenProcess(uint dwDesiredAccess, bool bInheritHandle, uint dwProcessId);
|
||||
|
||||
[DllImport("kernel32.dll", SetLastError = true, ExactSpelling = true)]
|
||||
private static extern IntPtr VirtualAllocEx(IntPtr hProcess, IntPtr lpAddress,
|
||||
uint dwSize, uint flAllocationType, uint flProtect);
|
||||
|
||||
[DllImport("kernel32.dll", SetLastError = true)]
|
||||
private static extern bool WriteProcessMemory(IntPtr hProcess, IntPtr lpBaseAddress,
|
||||
byte[] lpBuffer, uint nSize, out UIntPtr lpNumberOfBytesWritten);
|
||||
|
||||
[DllImport("kernel32.dll")]
|
||||
private static extern IntPtr CreateRemoteThread(IntPtr hProcess, IntPtr lpThreadAttributes,
|
||||
uint dwStackSize, IntPtr lpStartAddress, IntPtr lpParameter, uint dwCreationFlags, out uint lpThreadId);
|
||||
|
||||
[DllImport("kernel32.dll", CharSet = CharSet.Unicode)]
|
||||
private static extern IntPtr GetModuleHandle(string lpModuleName);
|
||||
|
||||
[DllImport("kernel32.dll", CharSet = CharSet.Ansi, ExactSpelling = true)]
|
||||
private static extern IntPtr GetProcAddress(IntPtr hModule, string procName);
|
||||
|
||||
[DllImport("kernel32.dll")]
|
||||
private static extern uint WaitForSingleObject(IntPtr hHandle, uint dwMilliseconds);
|
||||
|
||||
[DllImport("kernel32.dll")]
|
||||
private static extern bool VirtualFreeEx(IntPtr hProcess, IntPtr lpAddress,
|
||||
uint dwSize, uint dwFreeType);
|
||||
|
||||
private const uint PROCESS_ALL_ACCESS = 0x001F0FFF;
|
||||
private const uint MEM_COMMIT = 0x1000;
|
||||
private const uint MEM_RELEASE = 0x8000;
|
||||
private const uint PAGE_READWRITE = 0x04;
|
||||
|
||||
private delegate bool EnumWindowsProc(IntPtr hWnd, IntPtr lParam);
|
||||
|
||||
public TrayContext()
|
||||
{
|
||||
// Store agent path in registry
|
||||
StoreAgentPath();
|
||||
|
||||
// Create tray icon
|
||||
_trayIcon = new NotifyIcon
|
||||
{
|
||||
Text = "Decal Agent",
|
||||
Icon = LoadTrayIcon(),
|
||||
Visible = true,
|
||||
ContextMenuStrip = CreateContextMenu()
|
||||
};
|
||||
_trayIcon.DoubleClick += (s, e) => ShowAgentForm();
|
||||
|
||||
// Start timer to detect lobby windows for injection
|
||||
_lobbyTimer = new Timer { Interval = 1000 };
|
||||
_lobbyTimer.Tick += OnLobbyTimerTick;
|
||||
_lobbyTimer.Start();
|
||||
}
|
||||
|
||||
private static System.Drawing.Icon LoadTrayIcon()
|
||||
{
|
||||
// Try to load custom icon; fall back to default app icon
|
||||
string iconPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "decal.ico");
|
||||
if (File.Exists(iconPath))
|
||||
return new System.Drawing.Icon(iconPath);
|
||||
|
||||
return System.Drawing.SystemIcons.Application;
|
||||
}
|
||||
|
||||
private ContextMenuStrip CreateContextMenu()
|
||||
{
|
||||
var menu = new ContextMenuStrip();
|
||||
menu.Items.Add("Configure", null, (s, e) => ShowAgentForm());
|
||||
menu.Items.Add(new ToolStripSeparator());
|
||||
menu.Items.Add("Exit", null, (s, e) => ExitApplication());
|
||||
return menu;
|
||||
}
|
||||
|
||||
private void ShowAgentForm()
|
||||
{
|
||||
if (_agentForm == null || _agentForm.IsDisposed)
|
||||
{
|
||||
_agentForm = new AgentForm();
|
||||
}
|
||||
_agentForm.Show();
|
||||
_agentForm.BringToFront();
|
||||
_agentForm.Activate();
|
||||
}
|
||||
|
||||
private void StoreAgentPath()
|
||||
{
|
||||
try
|
||||
{
|
||||
string agentPath = AppDomain.CurrentDomain.BaseDirectory;
|
||||
if (agentPath.EndsWith(Path.DirectorySeparatorChar.ToString()))
|
||||
agentPath = agentPath.TrimEnd(Path.DirectorySeparatorChar);
|
||||
|
||||
using var key = Registry.LocalMachine.CreateSubKey(@"SOFTWARE\Decal\Agent");
|
||||
key?.SetValue("AgentPath", agentPath);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// May fail without admin rights - that's okay, path may already be set
|
||||
}
|
||||
}
|
||||
|
||||
private void OnLobbyTimerTick(object sender, EventArgs e)
|
||||
{
|
||||
// Enumerate windows looking for lobby/launcher windows to inject into
|
||||
EnumWindows(OnEnumWindow, IntPtr.Zero);
|
||||
}
|
||||
|
||||
private bool OnEnumWindow(IntPtr hWnd, IntPtr lParam)
|
||||
{
|
||||
var className = new char[256];
|
||||
int len = GetClassName(hWnd, className, className.Length);
|
||||
string name = new string(className, 0, len);
|
||||
|
||||
// Look for Asheron's Call lobby window class
|
||||
if (name != "ZoneLobbyWindow")
|
||||
return true;
|
||||
|
||||
GetWindowThreadProcessId(hWnd, out uint processId);
|
||||
if (processId == 0)
|
||||
return true;
|
||||
|
||||
// Check if already injected via named semaphore
|
||||
string semName = $"__LOBBYHOOK_{processId}";
|
||||
IntPtr hSem = CreateSemaphore(IntPtr.Zero, 0, 1, semName);
|
||||
if (hSem != IntPtr.Zero)
|
||||
{
|
||||
bool alreadyExists = Marshal.GetLastWin32Error() == 183; // ERROR_ALREADY_EXISTS
|
||||
CloseHandle(hSem);
|
||||
if (alreadyExists)
|
||||
return true;
|
||||
}
|
||||
|
||||
// Inject LauncherHook.DLL into the lobby process
|
||||
string agentPath = GetAgentPath();
|
||||
if (!string.IsNullOrEmpty(agentPath))
|
||||
{
|
||||
string hookDll = Path.Combine(agentPath, "LauncherHook.dll");
|
||||
if (File.Exists(hookDll))
|
||||
InjectDll(processId, hookDll);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static string GetAgentPath()
|
||||
{
|
||||
try
|
||||
{
|
||||
using var key = Registry.LocalMachine.OpenSubKey(@"SOFTWARE\Decal\Agent");
|
||||
return key?.GetValue("AgentPath") as string ?? "";
|
||||
}
|
||||
catch { return ""; }
|
||||
}
|
||||
|
||||
private static bool InjectDll(uint processId, string dllPath)
|
||||
{
|
||||
IntPtr hProcess = OpenProcess(PROCESS_ALL_ACCESS, false, processId);
|
||||
if (hProcess == IntPtr.Zero)
|
||||
return false;
|
||||
|
||||
try
|
||||
{
|
||||
byte[] pathBytes = System.Text.Encoding.Unicode.GetBytes(dllPath + '\0');
|
||||
uint pathSize = (uint)pathBytes.Length;
|
||||
|
||||
IntPtr remoteMem = VirtualAllocEx(hProcess, IntPtr.Zero, pathSize, MEM_COMMIT, PAGE_READWRITE);
|
||||
if (remoteMem == IntPtr.Zero)
|
||||
return false;
|
||||
|
||||
if (!WriteProcessMemory(hProcess, remoteMem, pathBytes, pathSize, out _))
|
||||
{
|
||||
VirtualFreeEx(hProcess, remoteMem, 0, MEM_RELEASE);
|
||||
return false;
|
||||
}
|
||||
|
||||
IntPtr kernel32 = GetModuleHandle("kernel32.dll");
|
||||
IntPtr loadLibW = GetProcAddress(kernel32, "LoadLibraryW");
|
||||
|
||||
IntPtr hThread = CreateRemoteThread(hProcess, IntPtr.Zero, 0,
|
||||
loadLibW, remoteMem, 0, out _);
|
||||
|
||||
if (hThread == IntPtr.Zero)
|
||||
{
|
||||
VirtualFreeEx(hProcess, remoteMem, 0, MEM_RELEASE);
|
||||
return false;
|
||||
}
|
||||
|
||||
WaitForSingleObject(hThread, 10000);
|
||||
CloseHandle(hThread);
|
||||
VirtualFreeEx(hProcess, remoteMem, 0, MEM_RELEASE);
|
||||
return true;
|
||||
}
|
||||
finally
|
||||
{
|
||||
CloseHandle(hProcess);
|
||||
}
|
||||
}
|
||||
|
||||
private void ExitApplication()
|
||||
{
|
||||
_lobbyTimer.Stop();
|
||||
_lobbyTimer.Dispose();
|
||||
_trayIcon.Visible = false;
|
||||
_trayIcon.Dispose();
|
||||
Application.Exit();
|
||||
}
|
||||
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
if (disposing)
|
||||
{
|
||||
_lobbyTimer?.Dispose();
|
||||
_trayIcon?.Dispose();
|
||||
_agentForm?.Dispose();
|
||||
}
|
||||
base.Dispose(disposing);
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue