commit f727066c0651a9f2371b26b03ab2be8fa29e84ed Author: Guillaume Langlois Date: Fri Oct 10 18:26:49 2025 -0400 feat: made prototype in c# diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a3fdf16 --- /dev/null +++ b/.gitignore @@ -0,0 +1,45 @@ +# Build results +bin/ +obj/ +out/ +[Bb]uild/ + + +# User-specific files +*.user +*.suo +*.userosscache +*.sln.docstates + +# Logs +*.log + +# OS-specific files +.DS_Store +Thumbs.db + +# Dotnet specific +*.dbmdl +*.jfm +*.pdb +*.mdb + +# NuGet +*.nupkg +*.snupkg +.nuget/ +packages/ +project.lock.json +project.fragment.lock.json + +# Rider / JetBrains +.idea/ + +# Visual Studio (if someone opens in full VS) +*.VC.db +*.VC.opendb + +# Others +*.swp +*.bak +*.tmp diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..1696836 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,21 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "name": "Conjure Arcade Overlay", + "type": "coreclr", + "request": "launch", + "program": "${workspaceFolder}/bin/Debug/net9.0-windows/Conjure.Arcade.Overlay.dll", + "args": ["${workspaceFolder}/Resources/placeholder_logo.png"], + "cwd": "${workspaceFolder}", + "stopAtEntry": false, + "console": "internalConsole", + "preLaunchTask": "build" + }, + { + "name": ".NET Core Attach", + "type": "coreclr", + "request": "attach" + }, + ] +} \ No newline at end of file diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 0000000..b7cc1a9 --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,54 @@ +{ + "version": "2.0.0", + "tasks": [ + { + "label": "build", + "command": "dotnet", + "type": "process", + "args": [ + "build", + "${workspaceFolder}/Conjure.Arcade.Overlay.csproj", + "/property:GenerateFullPaths=true", + "/consoleloggerparameters:NoSummary" + ], + "problemMatcher": "$msCompile", + "group": { + "kind": "build", + "isDefault": true + } + }, + { + "label": "Build Conjure.Arcade.Overlay (no dependencies)", + "type": "shell", + "command": "dotnet build Conjure.Arcade.Overlay.csproj --no-dependencies", + "group": "build", + "problemMatcher": "$msCompile" + }, + { + "label": "Clean all obj", // A descriptive name for your task + "type": "shell", + "command": "${workspaceFolder}/cleaning_bin_obj.bat", // Replace with the actual path + "group": { + "kind": "build", // Optional: Assign to a task group (e.g., build, test) + "isDefault": true // Optional: Make it the default for the group + }, + "presentation": { + "reveal": "always", // Show the terminal when the task runs + "panel": "new" // Open a new terminal panel for the task + }, + "problemMatcher": [] // Configure problem matching if needed + }, + { + "label": "Clean and Rebuild", + "dependsOrder": "sequence", + "dependsOn": [ + "Clean all bin and obj", + "Build Conjure.Arcade.Overlay (no dependencies)" + ], + "group": { + "kind": "build", + "isDefault": true + } + } + ] +} \ No newline at end of file diff --git a/Conjure.Arcade.Overlay.csproj b/Conjure.Arcade.Overlay.csproj new file mode 100644 index 0000000..c998232 --- /dev/null +++ b/Conjure.Arcade.Overlay.csproj @@ -0,0 +1,24 @@ + + + + Exe + net9.0-windows + enable + enable + true + + + true + false + Conjure Arcade Overlay + Conjure.Arcade.Overlay + 1.0.0 + + + + + PreserveNewest + + + + \ No newline at end of file diff --git a/Conjure.Arcade.Overlay.sln b/Conjure.Arcade.Overlay.sln new file mode 100644 index 0000000..e0e4e7f --- /dev/null +++ b/Conjure.Arcade.Overlay.sln @@ -0,0 +1,24 @@ +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.5.2.0 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Conjure.Arcade.Overlay", "Conjure.Arcade.Overlay.csproj", "{89927838-91A3-5909-5048-DCD37BEFC139}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {89927838-91A3-5909-5048-DCD37BEFC139}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {89927838-91A3-5909-5048-DCD37BEFC139}.Debug|Any CPU.Build.0 = Debug|Any CPU + {89927838-91A3-5909-5048-DCD37BEFC139}.Release|Any CPU.ActiveCfg = Release|Any CPU + {89927838-91A3-5909-5048-DCD37BEFC139}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {1AA29E9A-935E-4FE5-8E85-4D386D166F1C} + EndGlobalSection +EndGlobal diff --git a/MainOverlay.cs b/MainOverlay.cs new file mode 100644 index 0000000..fb1c6b5 --- /dev/null +++ b/MainOverlay.cs @@ -0,0 +1,126 @@ +using System; +using System.Diagnostics; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Input; +using System.Windows.Media; +using System.Windows.Interop; + +namespace Conjure.Arcade.Overlay +{ + public class MainOverlay : OverlayWindow + { + private readonly TickEngine _tickEngine; + private Process? _targetProcess; + private const string TARGET_PROCESS = "notepad"; + private readonly TextBlock _overlayText; + private bool _isPaused; + private HotkeyManager? _hotkeyManager; + private bool _isVisible; + + public MainOverlay() + { + _tickEngine = new TickEngine(Update, 60); + Loaded += OnLoaded; + Closed += OnClosed; + + // Set semi-transparent background + Background = new SolidColorBrush(Color.FromArgb(128, 0, 0, 0)); + + // Create and configure the text + _overlayText = new TextBlock + { + Text = "Overlay Active", + Foreground = Brushes.White, + FontSize = 24, + HorizontalAlignment = HorizontalAlignment.Center, + VerticalAlignment = VerticalAlignment.Center + }; + + // Add text to the window + Content = _overlayText; + + // Add key event handler + KeyDown += OnKeyDown; + + // Update text block + UpdateOverlayText(); + + // Start hidden + Visibility = Visibility.Hidden; + _isVisible = false; + } + + private void OnLoaded(object sender, RoutedEventArgs e) + { + var windowHandle = new WindowInteropHelper(this).Handle; + _hotkeyManager = new HotkeyManager(windowHandle, ToggleOverlay); + _hotkeyManager.Register(); + + _targetProcess = WindowUtils.FindProcess(TARGET_PROCESS); + if (_targetProcess == null) + { + MessageBox.Show($"Could not find process: {TARGET_PROCESS}"); + Close(); + return; + } + _tickEngine.Start(); + } + + private void OnClosed(object? sender, EventArgs e) + { + _hotkeyManager?.Unregister(); + _tickEngine.Stop(); + } + + private void Update() + { + if (_targetProcess?.HasExited ?? true) + { + Dispatcher.Invoke(Close); + return; + } + + var handle = _targetProcess.MainWindowHandle; + if (WindowUtils.GetWindowBounds(handle, out var rect)) + { + Dispatcher.Invoke(() => + { + Left = rect.Left; + Top = rect.Top; + Width = rect.Right - rect.Left; + Height = rect.Bottom - rect.Top; + }); + } + } + + private void ToggleOverlay() + { + _isVisible = !_isVisible; + _isPaused = _isVisible; // Pause when visible, unpause when hidden + + Visibility = _isVisible ? Visibility.Visible : Visibility.Hidden; + + if (_targetProcess != null) + { + if (_isPaused) + WindowUtils.SuspendProcess(_targetProcess); + else + WindowUtils.ResumeProcess(_targetProcess); + + UpdateOverlayText(); + } + } + + // Remove the space key handler since we're using the hotkey now + private void OnKeyDown(object sender, KeyEventArgs e) + { + // Empty or remove this method + } + + private void UpdateOverlayText() + { + _overlayText.Text = _isPaused ? "PAUSED (Ctrl+Alt+O to Resume)" : "Running"; + } + } +} diff --git a/Overlay/HotkeyManager.cs b/Overlay/HotkeyManager.cs new file mode 100644 index 0000000..dd0b9e7 --- /dev/null +++ b/Overlay/HotkeyManager.cs @@ -0,0 +1,50 @@ +using System; +using System.Runtime.InteropServices; +using System.Windows.Interop; + +namespace Conjure.Arcade.Overlay +{ + public class HotkeyManager + { + [DllImport("user32.dll")] + private static extern bool RegisterHotKey(IntPtr hWnd, int id, uint fsModifiers, uint vk); + + [DllImport("user32.dll")] + private static extern bool UnregisterHotKey(IntPtr hWnd, int id); + + private const int HOTKEY_ID = 9000; + private const uint MOD_ALT = 0x0001; + private const uint MOD_CONTROL = 0x0002; + + private readonly IntPtr _windowHandle; + private readonly Action _callback; + + public HotkeyManager(IntPtr windowHandle, Action callback) + { + _windowHandle = windowHandle; + _callback = callback; + ComponentDispatcher.ThreadPreprocessMessage += OnThreadPreprocessMessage; + } + + public void Register() + { + // Register Ctrl+Alt+O as the hotkey + RegisterHotKey(_windowHandle, HOTKEY_ID, MOD_CONTROL | MOD_ALT, 0x4F); // 0x4F is 'O' + } + + public void Unregister() + { + UnregisterHotKey(_windowHandle, HOTKEY_ID); + ComponentDispatcher.ThreadPreprocessMessage -= OnThreadPreprocessMessage; + } + + private void OnThreadPreprocessMessage(ref MSG msg, ref bool handled) + { + if (msg.message == 0x0312 && msg.wParam.ToInt32() == HOTKEY_ID) + { + _callback(); + handled = true; + } + } + } +} \ No newline at end of file diff --git a/Overlay/OverlayWindow.cs b/Overlay/OverlayWindow.cs new file mode 100644 index 0000000..687d545 --- /dev/null +++ b/Overlay/OverlayWindow.cs @@ -0,0 +1,55 @@ +using System; +using System.Windows; +using System.Windows.Interop; +using System.Runtime.InteropServices; + +namespace Conjure.Arcade.Overlay +{ + public class OverlayWindow : Window + { + [DllImport("user32.dll")] + private static extern int SetWindowLong(IntPtr hWnd, int nIndex, int dwNewLong); + + [DllImport("user32.dll")] + private static extern int GetWindowLong(IntPtr hWnd, int nIndex); + + private const int GWL_EXSTYLE = -20; + private const int WS_EX_TRANSPARENT = 0x20; + private const int WS_EX_LAYERED = 0x80000; + private bool _isClickThrough = true; + + public OverlayWindow() + { + // Make window transparent and click-through + WindowStyle = WindowStyle.None; + AllowsTransparency = true; + Background = null; + Topmost = true; + ShowInTaskbar = false; + + // Handle window creation + SourceInitialized += OnSourceInitialized; + } + + private void OnSourceInitialized(object? sender, EventArgs e) + { + var hwnd = new WindowInteropHelper(this).Handle; + var extendedStyle = GetWindowLong(hwnd, GWL_EXSTYLE); + SetWindowLong(hwnd, GWL_EXSTYLE, extendedStyle | WS_EX_TRANSPARENT | WS_EX_LAYERED); + } + + public void SetClickThrough(bool enabled) + { + var hwnd = new WindowInteropHelper(this).Handle; + var extendedStyle = GetWindowLong(hwnd, GWL_EXSTYLE); + + if (enabled) + extendedStyle |= WS_EX_TRANSPARENT; + else + extendedStyle &= ~WS_EX_TRANSPARENT; + + SetWindowLong(hwnd, GWL_EXSTYLE, extendedStyle); + _isClickThrough = enabled; + } + } +} \ No newline at end of file diff --git a/Overlay/Settings/SerializableSettings.cs b/Overlay/Settings/SerializableSettings.cs new file mode 100644 index 0000000..6a03583 --- /dev/null +++ b/Overlay/Settings/SerializableSettings.cs @@ -0,0 +1,76 @@ +using System; +using System.Globalization; +using System.IO; +using System.Text; + +namespace Conjure.Arcade.Overlay.Settings +{ + public class SerializableSettings : IDisposable where T : new() + { + private T? _current; // Make nullable + private bool _currentExists; + private readonly string _settingsPath; + + public SerializableSettings(string settingsName) + { + _settingsPath = Path.Combine( + AppDomain.CurrentDomain.BaseDirectory, + "Settings", + $"{settingsName}.json" + ); + } + + public T Current + { + get + { + if (!_currentExists) + { + _current = new T(); + _currentExists = true; + } + return _current!; // We know it's not null here + } + private set + { + _current = value; + _currentExists = true; + } + } + + public void Save() + { + var directory = Path.GetDirectoryName(_settingsPath); + if (!Directory.Exists(directory)) + Directory.CreateDirectory(directory!); + + File.WriteAllText(_settingsPath, + System.Text.Json.JsonSerializer.Serialize(Current, + new System.Text.Json.JsonSerializerOptions { WriteIndented = true })); + } + + public void Load() + { + if (!File.Exists(_settingsPath)) + { + Save(); + return; + } + + var json = File.ReadAllText(_settingsPath); + Current = System.Text.Json.JsonSerializer.Deserialize(json) ?? new T(); + } + + private bool _disposed; + public event EventHandler? OnDispose; + + public void Dispose() + { + if (!_disposed) + { + OnDispose?.Invoke(this, EventArgs.Empty); + _disposed = true; + } + } + } +} \ No newline at end of file diff --git a/Overlay/TickEngine.cs b/Overlay/TickEngine.cs new file mode 100644 index 0000000..ad6df7f --- /dev/null +++ b/Overlay/TickEngine.cs @@ -0,0 +1,62 @@ +using System; +using System.Diagnostics; +using System.Threading; +using System.Threading.Tasks; + +namespace Conjure.Arcade.Overlay +{ + public class TickEngine + { + private readonly Action _tickAction; + private readonly int _targetFps; + private readonly CancellationTokenSource _cancellationSource; + private bool _isRunning; + + public TickEngine(Action tickAction, int targetFps = 60) + { + _tickAction = tickAction; + _targetFps = targetFps; + _cancellationSource = new CancellationTokenSource(); + } + + public void Start() + { + if (_isRunning) return; + + _isRunning = true; + Task.Run(RunTickLoop, _cancellationSource.Token); + } + + public void Stop() + { + _isRunning = false; + _cancellationSource.Cancel(); + } + + private async Task RunTickLoop() + { + var stopwatch = new Stopwatch(); + var targetFrameTime = TimeSpan.FromSeconds(1.0 / _targetFps); + + while (_isRunning && !_cancellationSource.Token.IsCancellationRequested) + { + stopwatch.Restart(); + + try + { + _tickAction(); + } + catch (Exception ex) + { + Debug.WriteLine($"Error in tick loop: {ex}"); + } + + var elapsed = stopwatch.Elapsed; + if (elapsed < targetFrameTime) + { + await Task.Delay(targetFrameTime - elapsed); + } + } + } + } +} \ No newline at end of file diff --git a/Overlay/WindowUtils.cs b/Overlay/WindowUtils.cs new file mode 100644 index 0000000..6cba335 --- /dev/null +++ b/Overlay/WindowUtils.cs @@ -0,0 +1,89 @@ +using System; +using System.Diagnostics; +using System.Runtime.InteropServices; + +namespace Conjure.Arcade.Overlay +{ + public static class WindowUtils + { + [DllImport("kernel32.dll")] + private static extern IntPtr OpenThread(ThreadAccess dwDesiredAccess, bool bInheritHandle, uint dwThreadId); + + [DllImport("kernel32.dll")] + private static extern uint SuspendThread(IntPtr hThread); + + [DllImport("kernel32.dll")] + private static extern uint ResumeThread(IntPtr hThread); + + [DllImport("kernel32.dll")] + private static extern bool CloseHandle(IntPtr hObject); + + [Flags] + private enum ThreadAccess : int + { + SUSPEND_RESUME = 0x0002 + } + + [DllImport("user32.dll")] + private static extern IntPtr GetWindowRect(IntPtr hWnd, ref Rect rect); + + [StructLayout(LayoutKind.Sequential)] + public struct Rect + { + public int Left; + public int Top; + public int Right; + public int Bottom; + } + + public static bool GetWindowBounds(IntPtr handle, out Rect rect) + { + rect = new Rect(); + return GetWindowRect(handle, ref rect) != IntPtr.Zero; + } + + public static Process? FindProcess(string processName) + { + var processes = Process.GetProcessesByName(processName); + return processes.Length > 0 ? processes[0] : null; + } + + public static void SuspendProcess(Process process) + { + foreach (ProcessThread thread in process.Threads) + { + var threadHandle = OpenThread(ThreadAccess.SUSPEND_RESUME, false, (uint)thread.Id); + if (threadHandle != IntPtr.Zero) + { + try + { + SuspendThread(threadHandle); + } + finally + { + CloseHandle(threadHandle); + } + } + } + } + + public static void ResumeProcess(Process process) + { + foreach (ProcessThread thread in process.Threads) + { + var threadHandle = OpenThread(ThreadAccess.SUSPEND_RESUME, false, (uint)thread.Id); + if (threadHandle != IntPtr.Zero) + { + try + { + ResumeThread(threadHandle); + } + finally + { + CloseHandle(threadHandle); + } + } + } + } + } +} \ No newline at end of file diff --git a/Program.cs b/Program.cs new file mode 100644 index 0000000..293777e --- /dev/null +++ b/Program.cs @@ -0,0 +1,23 @@ +using System; +using System.Windows; +using Conjure.Arcade.Overlay.Views; + +namespace Conjure.Arcade.Overlay +{ + /// + /// + public class Program + { + /// + /// Defines the entry point of the application. + /// + [STAThread] + public static void Main(string[] args) + { + var logoPath = args.Length > 0 ? args[0] : "default_logo.png"; + var application = new Application(); + var mainWindow = new MainOverlayWindow(logoPath); + application.Run(mainWindow); + } + } +} \ No newline at end of file diff --git a/Resources/placeholder_logo.png b/Resources/placeholder_logo.png new file mode 100644 index 0000000..24cda82 Binary files /dev/null and b/Resources/placeholder_logo.png differ diff --git a/Views/MainOverlayWindow.xaml b/Views/MainOverlayWindow.xaml new file mode 100644 index 0000000..0b22a46 --- /dev/null +++ b/Views/MainOverlayWindow.xaml @@ -0,0 +1,114 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +