using System; using System.Diagnostics; using System.IO; using System.Runtime.InteropServices; using System.Windows.Forms; using Microsoft.Win32; namespace Decal.DenAgent { /// /// Application context that manages the system tray icon and lobby injection timer. /// Replaces the MFC CTrayWnd class. /// 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); } } }