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:
erik 2026-02-08 18:27:56 +01:00
commit d1442e3747
1382 changed files with 170725 additions and 0 deletions

View 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;
}
}
}
}

View 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>

View 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);
}
}
}
}

View 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());
}
}
}

View 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);
}
}
}