openDecal/Managed/Decal.DenAgent/TrayContext.cs
erik d1442e3747 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>
2026-02-08 18:27:56 +01:00

265 lines
9.6 KiB
C#

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