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