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>
265 lines
9.6 KiB
C#
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);
|
|
}
|
|
}
|
|
}
|