From f727066c0651a9f2371b26b03ab2be8fa29e84ed Mon Sep 17 00:00:00 2001 From: Guillaume Langlois Date: Fri, 10 Oct 2025 18:26:49 -0400 Subject: [PATCH] feat: made prototype in c# --- .gitignore | 45 +++++++ .vscode/launch.json | 21 +++ .vscode/tasks.json | 54 ++++++++ Conjure.Arcade.Overlay.csproj | 24 ++++ Conjure.Arcade.Overlay.sln | 24 ++++ MainOverlay.cs | 126 ++++++++++++++++++ Overlay/HotkeyManager.cs | 50 +++++++ Overlay/OverlayWindow.cs | 55 ++++++++ Overlay/Settings/SerializableSettings.cs | 76 +++++++++++ Overlay/TickEngine.cs | 62 +++++++++ Overlay/WindowUtils.cs | 89 +++++++++++++ Program.cs | 23 ++++ Resources/placeholder_logo.png | Bin 0 -> 6087 bytes Views/MainOverlayWindow.xaml | 114 ++++++++++++++++ Views/MainOverlayWindow.xaml.cs | 161 +++++++++++++++++++++++ cleaning_bin_obj.bat | 2 + 16 files changed, 926 insertions(+) create mode 100644 .gitignore create mode 100644 .vscode/launch.json create mode 100644 .vscode/tasks.json create mode 100644 Conjure.Arcade.Overlay.csproj create mode 100644 Conjure.Arcade.Overlay.sln create mode 100644 MainOverlay.cs create mode 100644 Overlay/HotkeyManager.cs create mode 100644 Overlay/OverlayWindow.cs create mode 100644 Overlay/Settings/SerializableSettings.cs create mode 100644 Overlay/TickEngine.cs create mode 100644 Overlay/WindowUtils.cs create mode 100644 Program.cs create mode 100644 Resources/placeholder_logo.png create mode 100644 Views/MainOverlayWindow.xaml create mode 100644 Views/MainOverlayWindow.xaml.cs create mode 100644 cleaning_bin_obj.bat 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 0000000000000000000000000000000000000000..24cda825d395c3d9cce2a4f0bfa405d2ee794dc9 GIT binary patch literal 6087 zcmZ{IcQjmI)b{AT1*6w6qK_eZi56Y--n(d{Mj0ien}{GfAw>A;y#>h_f+#_h=rdaM z5+Opud*6S)wZ1>zb=SFPt$Xfs&%Wn8d+%qTR3k$zDsonG5C}x2qpe{A^j$z}A|(dy zy-x3HfR5W$+r$6_iiChbaS0&MHSj2I7X%80f z|9h47Rb_)f;2s?fRkQG=qml?K^SQO*kNQaRa+X2ek|I~z%WFnO7CX&DHTnI z-aG1Fo!!6ARgag;v3R-!|2Eqq5>SbZ+enxXJ?t*zE>wS44l4;h<{~B=So0}Ws_UaU zI9F6w?)mL-)4A~HaNXMTsC~ZUEP93w(cAsgFCj7c{CK$O={WncuNTRJlM^3OK|x`& zR%yM=b@*l5ru%~o>reeP^sBjURWmatZsUbS71Ix%DSEt7q1wh$LM9y_wNvV#c@qXUM|i zg2aV{vExoEVb-N~2SUdu3qoA(dA7MWWC(#Eigi|&OK2}bU*#Cl2FAsOH`eC{!)TgVb=5EZSsnXruy$kWDrA~Ew zdxtlAN8hdsu9~*-Q6Orlz5x0ew0a|2S~^iWbs?T=0SwG;N~9p?tE;dX7;XmCsI;J2EC0(ul+b z1)H~^s#he0v)3~8_60s)CPvZuP0;bN;`rL3A*y4~IfK_}}_c3>lq}jEt9;(XmKBateN0GGBUe}Xm3XaIxw0433t~sV`xlFNy-P_3{E*D zS>Ra;>ZPiK|C{fomZ&w2OB4f_=Mz2Z-p~Uu_+uL_CMm(`q;s82*+F=bt;y&VENU~t znO~88{}*w5nd{4~AFgV4>>K5-@CPTazSGVw^?9AiOvIDWG~zsN|}1V}*5bx*X%rcew#>@3CF{O{b4l!f7iOs@EFU(|06 zq7BS?e0gD@Dwu4oqaKF5v3(H!@J}+$ePsyvOrj8MKO527+Cs}IPfoEjfobXk%8VOM8Jj;8eXg>IZdX=n2ms&EH20OnWRW;nxM8?;aSwY;;8p^+#R^ zty%TsD|7_p@rZnyoTOGMRU#lEA*nnVth7(eeU(>)+8qhh zPT>;o+$}GBIGn+u6{5lSzN!+ppqIx`%N19Rg1;`uJL9%GZCLFQVl9ic+$L;<$Ow>3 zXKucdjlqA(p#bUV(sBp?^NNmq%399L3;%l+ICMb7{RLZ7NW`;S5VhdHirv^K_lo1> zb~I zD-$VfK6IZQLH1fGT>1PyCVnF9SQmt|>67Fp7kG9j%MYxGVAk>8%i62#@@F%`-L7#{saxN(fAZJoPH2ny;n?JhLfV~`Q& z(5Hjif(yl&MD^ns1OqSH&(BXH!80knPIYNXS~k(Z!0kxI=>GL65`QxKWUUXyIF{=S zpL*nbhhRIJN}XC#=ZrHt{c-(UtJb2vnz&}(LpK^g#HC1lb^SS*nvv>YZJnAS9q<`j z_T1`2B-NL(G4Eu(@$9v=Hg#G`w{6yH{+KNBPM&&jHvMPhdVoOA z8@g|da}Ii`)SR+2QRH)3Ycl55j-tiVD*FENT56s{<=$Z0(=rT)7tHMS6+S0bTf$2k z4cnhk&#~q`+&tQx=p5r+l91NuLuPO)C3zJ2@GLyOR5y)iThb(z;=Zb$#!0#MOtfc` zH7KsJMyHX@-VgukpoH7r)|7l-+AZDTziQKJZ(*~QD)7b#QQrMp5fYyNGkQJrPeA}l z?of_8Dx00NG8fy?(LqW@Rmt%!w4Wm%`vq5l6H_aDiTy`F@@ZnVBuw7}q}LU8N<&)q zPxPDD)SuHZx|tF(jJhZqGk}@non*3O^KQ(Z$v?$$X@)0J?%c=cA@d)RbB zup#|usnc>xNtLqPxdpN$5oCP->eXAGr-j`PqwUk5KPR+= z1)pB8s-Z#VS4qMv;J>-P4R^(t!?($LT_n|F|nZ=2Kj zuqDN(BK8r;Hy_thcA;u9=M@auD-GG7wQkLJo_pCo9zEB_@zU_+>D+;LvUAQZBBh74 zG7{73>tzFvo0xu0fdnwsHbLzaleQ5yR?LnlcBM!{56R{aH92+@NP2Uds>u?m<%$11 zlo59Xy}ve4Y7X|*x}$SKvc~SKeZeHcxuQ&2-pu${ZU@8pMFpv8Y2-8NX1`UxR~u2o zx=+@I-f8SPZVrbRtD@0X-I0ui(tf&;DNp-N<{+}h(8|in{NjARp{OmEgkO8hBq#pg zL7twTJjupP_6A6LqM(yCC4F7}f#wP=@8RX$-T6KRIz6VYmD2y(SKMp0pF81<6G~?#XtT@uv0n;p$oqc*h*vO&_@dqr5w&19Lr3O?nogtFMJ^hdL8x^4nVNMH-9$zR1I zTd=adou8YV1}H~z9+-B-6-I?j1##E+3d4|X=MC6US6+_a`u^)DAOJB^XE>8FGBW1p zW~GTgJH8i`&zmJIDBHZUC+R^xK}z`r5R-0`Z_?|z{OE%653_{lXbeNLzI@Sr_q8~+ zZ#||;L!YuBUnW-B+ut%~UsK!dt0yeu&p{$xaljwjhHnXAR^YV_VKaLdvY z?c(mfdv=R|_`To$%ekbP?s`mgv;V)yq2y&yMthesu6cltEbg98VCduT+c}IuH4Mlznqreo= zFUFc&{8zf3cU5~8i#(tW%6A$aA_WFj93qH{Wc;!^l9&sBTplgw(=v%4z-{%20K3}x z^_tG&Y20ggljbv{y6X9n^kaR$+854luc=^`bm|8qaw`_q*ukJis}9P8^=QktLM~n$ z-AT*j5mV|Szl(&|J{6kRzHa1CGUwig+nS(LPmnnQeThIbXbfYvcy5OA(*d?G>O$cJ zJHZ$*)xegNL(9gMJk;3ByCuq*p))A}P{YTv5uN5f=CweE`9iC(lB)-BcHOp<8W8Oz zVfN5E1__KG+r46|W2%$-VIRHRsRn|i99J~lmaQxXow|MNgt@W$xnJAXSTe2M~2q>6xz*FgT1~uNI|9T*ssom6BcjIzuu-Y zIbN|H9k@WBUTtQmKzYPClYg>u`ujXFnPOV*z$Fq|PM>QV}y# z5;Om$JiYY3svnp3c%xOI@a*tY@3?;Trn&Nq&vAWyN{A47;H6!v$XzO$#LLUe$Jr2r zgq)%Fv$2v7(AY^kh>SXzot?ex{33C!EV;DQ(ZJ&}az<~YBEpb$9P81_a<_CNZg+cO z$-oFM2f~46TAGoSRZ2?>Y$+5oMQgxb^r&&+Zm#V#>U;0*RP+7Aqg0#GTo=r;#($_q z-^V&SI-+~1nmYyn+(f#)y&dB7T9rsZpsKLrtik|A%^^=}9cQYWKrxxT===Gk(|<0v ze=^<3*zoUha1y!qe4VucWS)%@`nRB9xjjI@&8v20mK9ZQCA{n_y}6TKbm4hd&dcD~ zjZ7mCBdEsnWMX2bY%c;w#^d=dcQC@}o)c%JL(C?=YG;oo6v+etM%*4cBDTqegoVA6 zz0KRartyhYKH3;i#Fug@Dk{#31=-6AC{{lX5(8YR!SUm5-mg&qgpCasGjsE4YSWxH z-^HJAM`B_x?jKwbcd6hS9%k0!sCtg!)&|;>iPK(n-FSdgaebg`aOfkinATR%Ukvod zA*U;bXP?@1b$3rwkC}+{-)KAupJFvPH}AS#4tQwrW}gkW9B_%f{dd!b$$l{|7cAko z6SCw?nz$sa55}|7zS-X%EVLl-TI-c-2@1k4&E$kVxG)Bl<(UZFKl)~!ko?S2J(irI zj@vq4HUehT;j5!SsgshPVHv-IGX5x>HcF-4sT?lu^_LhP7^oWbl>7*)v5CdTrDh8V zMtn(9KG$qJz&AQJ7)Hp%2{D(x0?9mkw&a|`%L}^)2>Kv~`u(pBJhMN)j}i-Y;T1mg zN%EGI`i-e0W@BU1*U}M>vgx1 zrqZwo?Jxpad3l$|*CI;`u5^Ol8Wa{bl_ONn0Fp=hs+o>XL$YH|!^w#IFTr2$^Q5iE>g;y#Bb3Y5H&m;0&8(Kl1n>N!##Xt~yc zk`fNs&iGr7`@js|d`bf8Vo?O?ws40eYObWXIM9+aynNw6B7B5)$+MQd}j z>ds1co9h;zoM?_4*q~y34Mbm@_vyLn?&=4bBj|pm--j?#3Ysd8q#9>rw%b|6xtSb~ zlkQKmtZl|wrY zXO;{TX+M9W^QI=vBg?`GsY2bTBoKHAtweZ4rz0IEdedIr&QY4pQ*Kxl7# z9u^K$mgbv> zb!&ct$+8sZnAzB*sS@BDo0#zK=_n~FonfB;`{dU|sHTi^5SiweKTF#R^dtaCB7c6{ozTWB;nvB_zQeK89r& zpPvV_1jC4!5^Szm9e!1GYbosvjI(gc;>0c*$cUXUh7_;PX8L) z8>0i?@Bk$-``!`4yrR2^Yi~kgqM+k!`E&!V>?|NK)>*fcgM8xk@EXHef?54hH@hLs z^1jSRK21WmcXP2=K=}Z6q)oNk$|O*-*_?E2y=IW+Fi`7Xy5R3$ZLQi_KUB>Kfm>_= za*0+RLC9HuBJH>2HfFyOEnkGIM8S+kdWT+uP1ZV z^gXPbf4~jqZ1~tiv>Ol7_4-tNrQnT&>WC&?7aIdjLuPP&+Rc#hD5`ahdy)bK&pm2j9chO#u_qO2ZhUV*ld#`}-r@CD0LF zv3JS?>RPy&A=HJxCsL0lAo2F^wN$bUdKCb4XE z7&^z{x*|3q)3)@gM>QgL|>1 z;E + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +