From 711768d06437d8d505db6ea6fb5b7fef12b85016 Mon Sep 17 00:00:00 2001 From: dsarno Date: Sat, 3 Jan 2026 12:42:32 -0800 Subject: [PATCH] Async Test Infrastructure & Editor Readiness Status + new refresh_unity tool (#507) * Add editor readiness v2, refresh tool, and preflight guards * Detect external package changes and harden refresh retry * feat: add TestRunnerNoThrottle and async test running with background stall prevention - Add TestRunnerNoThrottle.cs: Sets editor to 'No Throttling' mode during test runs with SessionState persistence across domain reload - Add run_tests_async and get_test_job tools for non-blocking test execution - Add TestJobManager for async test job tracking with progress monitoring - Add ForceSynchronousImport to all AssetDatabase.Refresh() calls to prevent stalls - Mark DomainReloadResilienceTests as [Explicit] with documentation explaining the test infrastructure limitation (internal coroutine waits vs MCP socket polling) - MCP workflow is unaffected - socket messages provide external stimulus that keeps Unity responsive even when backgrounded * refactor: simplify and clean up code - Remove unused Newtonsoft.Json.Linq import from TestJobManager - Add throttling to SessionState persistence (once per second) to reduce overhead - Critical job state changes (start/finish) still persist immediately - Fix duplicate XML summary tag in DomainReloadResilienceTests * docs: add async test tools to README, document domain reload limitation - Add run_tests_async and get_test_job to main README tools list - Document background stall limitation for domain reload tests in DEV readme * ci: add separate job for domain reload tests Run [Explicit] domain_reload tests in their own job using -testCategory * ci: run domain reload tests in same job as regular tests Combines into single job with two test steps to reuse cached Library * fix: address coderabbit review issues - Fix TOCTOU race in TestJobManager.StartJob (single lock scope for check-and-set) - Store TestRunnerApi reference with HideAndDontSave to prevent GC/serialization issues * docs: update tool descriptions to prefer run_tests_async - run_tests_async is now marked as preferred for long-running suites - run_tests description notes it blocks and suggests async alternative * docs: update README screenshot to v8.6 UI * docs: add v8.6 UI screenshot * Update README for MCP version and instructions for v8.7 * fix: handle preflight busy signals and derive job status from test results - manage_asset, manage_gameobject, manage_scene now check preflight return value and propagate busy/retry signals to clients (fixes Sourcery #1) - TestJobManager.FinalizeCurrentJobFromRunFinished now sets job status to Failed when resultPayload.Failed > 0, not always Succeeded (fixes Sourcery #2) * fix: increase HTTP server startup timeout for dev mode When 'Force fresh server install' is enabled, uvx uses --no-cache --refresh which rebuilds the package and takes significantly longer to start. - Increase timeout from 10s to 45s when dev mode is enabled - Add informative log message explaining the longer startup time - Show actual timeout value in warning message * fix: derive job status from test results in FinalizeFromTask fallback Apply same logic as FinalizeCurrentJobFromRunFinished: check result.Failed > 0 to correctly mark jobs as Failed when tests fail, even in the fallback path when RunFinished callback is not delivered. --- .github/workflows/unity-tests.yml | 18 +- .../Editor/Resources/Editor/EditorStateV2.cs | 29 + .../Resources/Editor/EditorStateV2.cs.meta | 11 + .../Editor/Services/EditorStateCache.cs | 234 +++++++ .../Editor/Services/EditorStateCache.cs.meta | 11 + MCPForUnity/Editor/Services/TestJobManager.cs | 586 ++++++++++++++++++ .../Editor/Services/TestJobManager.cs.meta | 13 + MCPForUnity/Editor/Services/TestRunStatus.cs | 62 ++ .../Editor/Services/TestRunStatus.cs.meta | 11 + .../Editor/Services/TestRunnerNoThrottle.cs | 139 +++++ .../Services/TestRunnerNoThrottle.cs.meta | 11 + .../Editor/Services/TestRunnerService.cs | 97 ++- MCPForUnity/Editor/Tools/GetTestJob.cs | 54 ++ MCPForUnity/Editor/Tools/GetTestJob.cs.meta | 13 + MCPForUnity/Editor/Tools/ManageAsset.cs | 4 +- MCPForUnity/Editor/Tools/ManageGameObject.cs | 2 +- MCPForUnity/Editor/Tools/ManageScene.cs | 6 +- MCPForUnity/Editor/Tools/ManageScript.cs | 2 +- MCPForUnity/Editor/Tools/ManageShader.cs | 6 +- .../Editor/Tools/Prefabs/ManagePrefabs.cs | 2 +- MCPForUnity/Editor/Tools/RefreshUnity.cs | 175 ++++++ MCPForUnity/Editor/Tools/RefreshUnity.cs.meta | 11 + MCPForUnity/Editor/Tools/RunTestsAsync.cs | 128 ++++ .../Editor/Tools/RunTestsAsync.cs.meta | 13 + .../Connection/McpConnectionSection.cs | 14 +- README.md | 18 +- .../src/services/resources/editor_state_v2.py | 270 ++++++++ .../state/external_changes_scanner.py | 246 ++++++++ Server/src/services/tools/manage_asset.py | 7 + .../src/services/tools/manage_gameobject.py | 5 + Server/src/services/tools/manage_scene.py | 4 + Server/src/services/tools/preflight.py | 107 ++++ Server/src/services/tools/refresh_unity.py | 90 +++ Server/src/services/tools/run_tests.py | 25 +- Server/src/services/tools/test_jobs.py | 94 +++ .../test_editor_state_v2_contract.py | 58 ++ .../test_external_changes_scanner.py | 86 +++ .../test_refresh_unity_registration.py | 14 + .../test_refresh_unity_retry_recovery.py | 46 ++ .../test_run_tests_busy_semantics.py | 36 ++ .../tests/integration/test_test_jobs_async.py | 52 ++ Server/uv.lock | 2 +- .../Tools/DomainReloadResilienceTests.cs | 22 +- .../Tools/ManageScriptableObjectTests.cs | 71 ++- docs/README-DEV.md | 13 +- docs/images/unity-mcp-ui-v8.6.png | Bin 0 -> 148984 bytes 46 files changed, 2861 insertions(+), 57 deletions(-) create mode 100644 MCPForUnity/Editor/Resources/Editor/EditorStateV2.cs create mode 100644 MCPForUnity/Editor/Resources/Editor/EditorStateV2.cs.meta create mode 100644 MCPForUnity/Editor/Services/EditorStateCache.cs create mode 100644 MCPForUnity/Editor/Services/EditorStateCache.cs.meta create mode 100644 MCPForUnity/Editor/Services/TestJobManager.cs create mode 100644 MCPForUnity/Editor/Services/TestJobManager.cs.meta create mode 100644 MCPForUnity/Editor/Services/TestRunStatus.cs create mode 100644 MCPForUnity/Editor/Services/TestRunStatus.cs.meta create mode 100644 MCPForUnity/Editor/Services/TestRunnerNoThrottle.cs create mode 100644 MCPForUnity/Editor/Services/TestRunnerNoThrottle.cs.meta create mode 100644 MCPForUnity/Editor/Tools/GetTestJob.cs create mode 100644 MCPForUnity/Editor/Tools/GetTestJob.cs.meta create mode 100644 MCPForUnity/Editor/Tools/RefreshUnity.cs create mode 100644 MCPForUnity/Editor/Tools/RefreshUnity.cs.meta create mode 100644 MCPForUnity/Editor/Tools/RunTestsAsync.cs create mode 100644 MCPForUnity/Editor/Tools/RunTestsAsync.cs.meta create mode 100644 Server/src/services/resources/editor_state_v2.py create mode 100644 Server/src/services/state/external_changes_scanner.py create mode 100644 Server/src/services/tools/preflight.py create mode 100644 Server/src/services/tools/refresh_unity.py create mode 100644 Server/src/services/tools/test_jobs.py create mode 100644 Server/tests/integration/test_editor_state_v2_contract.py create mode 100644 Server/tests/integration/test_external_changes_scanner.py create mode 100644 Server/tests/integration/test_refresh_unity_registration.py create mode 100644 Server/tests/integration/test_refresh_unity_retry_recovery.py create mode 100644 Server/tests/integration/test_run_tests_busy_semantics.py create mode 100644 Server/tests/integration/test_test_jobs_async.py create mode 100644 docs/images/unity-mcp-ui-v8.6.png diff --git a/.github/workflows/unity-tests.yml b/.github/workflows/unity-tests.yml index 632a1f8..191c161 100644 --- a/.github/workflows/unity-tests.yml +++ b/.github/workflows/unity-tests.yml @@ -25,13 +25,11 @@ jobs: unityVersion: - 2021.3.45f2 steps: - # Checkout - name: Checkout repository uses: actions/checkout@v4 with: lfs: true - # Cache - uses: actions/cache@v4 with: path: ${{ matrix.projectPath }}/Library @@ -40,7 +38,20 @@ jobs: Library-${{ matrix.projectPath }}- Library- - # Test + # Run domain reload tests first (they're [Explicit] so need explicit category) + - name: Run domain reload tests + uses: game-ci/unity-test-runner@v4 + id: domain-tests + env: + UNITY_EMAIL: ${{ secrets.UNITY_EMAIL }} + UNITY_PASSWORD: ${{ secrets.UNITY_PASSWORD }} + UNITY_LICENSE: ${{ secrets.UNITY_LICENSE }} + with: + projectPath: ${{ matrix.projectPath }} + unityVersion: ${{ matrix.unityVersion }} + testMode: ${{ matrix.testMode }} + customParameters: -testCategory domain_reload + - name: Run tests uses: game-ci/unity-test-runner@v4 id: tests @@ -53,7 +64,6 @@ jobs: unityVersion: ${{ matrix.unityVersion }} testMode: ${{ matrix.testMode }} - # Upload test results - uses: actions/upload-artifact@v4 if: always() with: diff --git a/MCPForUnity/Editor/Resources/Editor/EditorStateV2.cs b/MCPForUnity/Editor/Resources/Editor/EditorStateV2.cs new file mode 100644 index 0000000..33ac970 --- /dev/null +++ b/MCPForUnity/Editor/Resources/Editor/EditorStateV2.cs @@ -0,0 +1,29 @@ +using System; +using MCPForUnity.Editor.Helpers; +using MCPForUnity.Editor.Services; +using Newtonsoft.Json.Linq; + +namespace MCPForUnity.Editor.Resources.Editor +{ + /// + /// Provides a cached, v2 readiness snapshot. This is designed to remain responsive even when Unity is busy. + /// + [McpForUnityResource("get_editor_state_v2")] + public static class EditorStateV2 + { + public static object HandleCommand(JObject @params) + { + try + { + var snapshot = EditorStateCache.GetSnapshot(); + return new SuccessResponse("Retrieved editor state (v2).", snapshot); + } + catch (Exception e) + { + return new ErrorResponse($"Error getting editor state (v2): {e.Message}"); + } + } + } +} + + diff --git a/MCPForUnity/Editor/Resources/Editor/EditorStateV2.cs.meta b/MCPForUnity/Editor/Resources/Editor/EditorStateV2.cs.meta new file mode 100644 index 0000000..e776994 --- /dev/null +++ b/MCPForUnity/Editor/Resources/Editor/EditorStateV2.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 5514ec4eb8a294a55892a13194e250e8 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/MCPForUnity/Editor/Services/EditorStateCache.cs b/MCPForUnity/Editor/Services/EditorStateCache.cs new file mode 100644 index 0000000..9755784 --- /dev/null +++ b/MCPForUnity/Editor/Services/EditorStateCache.cs @@ -0,0 +1,234 @@ +using System; +using MCPForUnity.Editor.Helpers; +using Newtonsoft.Json.Linq; +using UnityEditor; +using UnityEditorInternal; +using UnityEditor.SceneManagement; +using UnityEngine; + +namespace MCPForUnity.Editor.Services +{ + /// + /// Maintains a cached readiness snapshot (v2) so status reads remain fast even when Unity is busy. + /// Updated on the main thread via Editor callbacks and periodic update ticks. + /// + [InitializeOnLoad] + internal static class EditorStateCache + { + private static readonly object LockObj = new(); + private static long _sequence; + private static long _observedUnixMs; + + private static bool _lastIsCompiling; + private static long? _lastCompileStartedUnixMs; + private static long? _lastCompileFinishedUnixMs; + + private static bool _domainReloadPending; + private static long? _domainReloadBeforeUnixMs; + private static long? _domainReloadAfterUnixMs; + + private static double _lastUpdateTimeSinceStartup; + private const double MinUpdateIntervalSeconds = 0.25; + + private static JObject _cached; + + static EditorStateCache() + { + try + { + _sequence = 0; + _observedUnixMs = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); + _cached = BuildSnapshot("init"); + + EditorApplication.update += OnUpdate; + EditorApplication.playModeStateChanged += _ => ForceUpdate("playmode"); + + AssemblyReloadEvents.beforeAssemblyReload += () => + { + _domainReloadPending = true; + _domainReloadBeforeUnixMs = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); + ForceUpdate("before_domain_reload"); + }; + AssemblyReloadEvents.afterAssemblyReload += () => + { + _domainReloadPending = false; + _domainReloadAfterUnixMs = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); + ForceUpdate("after_domain_reload"); + }; + } + catch (Exception ex) + { + McpLog.Error($"[EditorStateCache] Failed to initialise: {ex.Message}\n{ex.StackTrace}"); + } + } + + private static void OnUpdate() + { + // Throttle to reduce overhead while keeping the snapshot fresh enough for polling clients. + double now = EditorApplication.timeSinceStartup; + if (now - _lastUpdateTimeSinceStartup < MinUpdateIntervalSeconds) + { + // Still update on compilation edge transitions to keep timestamps meaningful. + bool isCompiling = EditorApplication.isCompiling; + if (isCompiling == _lastIsCompiling) + { + return; + } + } + + _lastUpdateTimeSinceStartup = now; + ForceUpdate("tick"); + } + + private static void ForceUpdate(string reason) + { + lock (LockObj) + { + _cached = BuildSnapshot(reason); + } + } + + private static JObject BuildSnapshot(string reason) + { + _sequence++; + _observedUnixMs = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); + + bool isCompiling = EditorApplication.isCompiling; + if (isCompiling && !_lastIsCompiling) + { + _lastCompileStartedUnixMs = _observedUnixMs; + } + else if (!isCompiling && _lastIsCompiling) + { + _lastCompileFinishedUnixMs = _observedUnixMs; + } + _lastIsCompiling = isCompiling; + + var scene = EditorSceneManager.GetActiveScene(); + string scenePath = string.IsNullOrEmpty(scene.path) ? null : scene.path; + string sceneGuid = !string.IsNullOrEmpty(scenePath) ? AssetDatabase.AssetPathToGUID(scenePath) : null; + + bool testsRunning = TestRunStatus.IsRunning; + var testsMode = TestRunStatus.Mode?.ToString(); + string currentJobId = TestJobManager.CurrentJobId; + bool isFocused = InternalEditorUtility.isApplicationActive; + + var activityPhase = "idle"; + if (testsRunning) + { + activityPhase = "running_tests"; + } + else if (isCompiling) + { + activityPhase = "compiling"; + } + else if (_domainReloadPending) + { + activityPhase = "domain_reload"; + } + else if (EditorApplication.isUpdating) + { + activityPhase = "asset_import"; + } + else if (EditorApplication.isPlayingOrWillChangePlaymode) + { + activityPhase = "playmode_transition"; + } + + // Keep this as a plain JSON object for minimal friction with transports. + return JObject.FromObject(new + { + schema_version = "unity-mcp/editor_state@2", + observed_at_unix_ms = _observedUnixMs, + sequence = _sequence, + unity = new + { + instance_id = (string)null, + unity_version = Application.unityVersion, + project_id = (string)null, + platform = Application.platform.ToString(), + is_batch_mode = Application.isBatchMode + }, + editor = new + { + is_focused = isFocused, + play_mode = new + { + is_playing = EditorApplication.isPlaying, + is_paused = EditorApplication.isPaused, + is_changing = EditorApplication.isPlayingOrWillChangePlaymode + }, + active_scene = new + { + path = scenePath, + guid = sceneGuid, + name = scene.name ?? string.Empty + } + }, + activity = new + { + phase = activityPhase, + since_unix_ms = _observedUnixMs, + reasons = new[] { reason } + }, + compilation = new + { + is_compiling = isCompiling, + is_domain_reload_pending = _domainReloadPending, + last_compile_started_unix_ms = _lastCompileStartedUnixMs, + last_compile_finished_unix_ms = _lastCompileFinishedUnixMs, + last_domain_reload_before_unix_ms = _domainReloadBeforeUnixMs, + last_domain_reload_after_unix_ms = _domainReloadAfterUnixMs + }, + assets = new + { + is_updating = EditorApplication.isUpdating, + external_changes_dirty = false, + external_changes_last_seen_unix_ms = (long?)null, + refresh = new + { + is_refresh_in_progress = false, + last_refresh_requested_unix_ms = (long?)null, + last_refresh_finished_unix_ms = (long?)null + } + }, + tests = new + { + is_running = testsRunning, + mode = testsMode, + current_job_id = string.IsNullOrEmpty(currentJobId) ? null : currentJobId, + started_unix_ms = TestRunStatus.StartedUnixMs, + started_by = "unknown", + last_run = TestRunStatus.FinishedUnixMs.HasValue + ? new + { + finished_unix_ms = TestRunStatus.FinishedUnixMs, + result = "unknown", + counts = (object)null + } + : null + }, + transport = new + { + unity_bridge_connected = (bool?)null, + last_message_unix_ms = (long?)null + } + }); + } + + public static JObject GetSnapshot() + { + lock (LockObj) + { + // Defensive: if something went wrong early, rebuild once. + if (_cached == null) + { + _cached = BuildSnapshot("rebuild"); + } + return (JObject)_cached.DeepClone(); + } + } + } +} + + diff --git a/MCPForUnity/Editor/Services/EditorStateCache.cs.meta b/MCPForUnity/Editor/Services/EditorStateCache.cs.meta new file mode 100644 index 0000000..21c5d01 --- /dev/null +++ b/MCPForUnity/Editor/Services/EditorStateCache.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: aa7909967ce3c48c493181c978782a54 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/MCPForUnity/Editor/Services/TestJobManager.cs b/MCPForUnity/Editor/Services/TestJobManager.cs new file mode 100644 index 0000000..d5399cf --- /dev/null +++ b/MCPForUnity/Editor/Services/TestJobManager.cs @@ -0,0 +1,586 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using MCPForUnity.Editor.Helpers; +using Newtonsoft.Json; +using UnityEditor; +using UnityEditorInternal; +using UnityEditor.TestTools.TestRunner.Api; + +namespace MCPForUnity.Editor.Services +{ + internal enum TestJobStatus + { + Running, + Succeeded, + Failed + } + + internal sealed class TestJobFailure + { + public string FullName { get; set; } + public string Message { get; set; } + } + + internal sealed class TestJob + { + public string JobId { get; set; } + public TestJobStatus Status { get; set; } + public string Mode { get; set; } + public long StartedUnixMs { get; set; } + public long? FinishedUnixMs { get; set; } + public long LastUpdateUnixMs { get; set; } + public int? TotalTests { get; set; } + public int CompletedTests { get; set; } + public string CurrentTestFullName { get; set; } + public long? CurrentTestStartedUnixMs { get; set; } + public string LastFinishedTestFullName { get; set; } + public long? LastFinishedUnixMs { get; set; } + public List FailuresSoFar { get; set; } + public string Error { get; set; } + public TestRunResult Result { get; set; } + } + + /// + /// Tracks async test jobs started via MCP tools. This is not intended to capture manual Test Runner UI runs. + /// + internal static class TestJobManager + { + // Keep this small to avoid ballooning payloads during polling. + private const int FailureCap = 25; + private const long StuckThresholdMs = 60_000; + private const int MaxJobsToKeep = 10; + private const long MinPersistIntervalMs = 1000; // Throttle persistence to reduce overhead + + // SessionState survives domain reloads within the same Unity Editor session. + private const string SessionKeyJobs = "MCPForUnity.TestJobsV1"; + private const string SessionKeyCurrentJobId = "MCPForUnity.CurrentTestJobIdV1"; + + private static readonly object LockObj = new(); + private static readonly Dictionary Jobs = new(); + private static string _currentJobId; + private static long _lastPersistUnixMs; + + static TestJobManager() + { + // Restore after domain reloads (e.g., compilation while a job is running). + TryRestoreFromSessionState(); + } + + public static string CurrentJobId + { + get { lock (LockObj) return _currentJobId; } + } + + public static bool HasRunningJob + { + get + { + lock (LockObj) + { + return !string.IsNullOrEmpty(_currentJobId); + } + } + } + + private sealed class PersistedState + { + public string current_job_id { get; set; } + public List jobs { get; set; } + } + + private sealed class PersistedJob + { + public string job_id { get; set; } + public string status { get; set; } + public string mode { get; set; } + public long started_unix_ms { get; set; } + public long? finished_unix_ms { get; set; } + public long last_update_unix_ms { get; set; } + public int? total_tests { get; set; } + public int completed_tests { get; set; } + public string current_test_full_name { get; set; } + public long? current_test_started_unix_ms { get; set; } + public string last_finished_test_full_name { get; set; } + public long? last_finished_unix_ms { get; set; } + public List failures_so_far { get; set; } + public string error { get; set; } + } + + private static TestJobStatus ParseStatus(string status) + { + if (string.IsNullOrWhiteSpace(status)) + { + return TestJobStatus.Running; + } + + string s = status.Trim().ToLowerInvariant(); + return s switch + { + "succeeded" => TestJobStatus.Succeeded, + "failed" => TestJobStatus.Failed, + _ => TestJobStatus.Running + }; + } + + private static void TryRestoreFromSessionState() + { + try + { + string json = SessionState.GetString(SessionKeyJobs, string.Empty); + if (string.IsNullOrWhiteSpace(json)) + { + var legacy = SessionState.GetString(SessionKeyCurrentJobId, string.Empty); + _currentJobId = string.IsNullOrWhiteSpace(legacy) ? null : legacy; + return; + } + + var state = JsonConvert.DeserializeObject(json); + if (state?.jobs == null) + { + return; + } + + lock (LockObj) + { + Jobs.Clear(); + foreach (var pj in state.jobs) + { + if (pj == null || string.IsNullOrWhiteSpace(pj.job_id)) + { + continue; + } + + Jobs[pj.job_id] = new TestJob + { + JobId = pj.job_id, + Status = ParseStatus(pj.status), + Mode = pj.mode, + StartedUnixMs = pj.started_unix_ms, + FinishedUnixMs = pj.finished_unix_ms, + LastUpdateUnixMs = pj.last_update_unix_ms, + TotalTests = pj.total_tests, + CompletedTests = pj.completed_tests, + CurrentTestFullName = pj.current_test_full_name, + CurrentTestStartedUnixMs = pj.current_test_started_unix_ms, + LastFinishedTestFullName = pj.last_finished_test_full_name, + LastFinishedUnixMs = pj.last_finished_unix_ms, + FailuresSoFar = pj.failures_so_far ?? new List(), + Error = pj.error, + // Intentionally not persisted to avoid ballooning SessionState. + Result = null + }; + } + + _currentJobId = string.IsNullOrWhiteSpace(state.current_job_id) ? null : state.current_job_id; + if (!string.IsNullOrEmpty(_currentJobId) && !Jobs.ContainsKey(_currentJobId)) + { + _currentJobId = null; + } + } + } + catch (Exception ex) + { + // Restoration is best-effort; never block editor load. + McpLog.Warn($"[TestJobManager] Failed to restore SessionState: {ex.Message}"); + } + } + + private static void PersistToSessionState(bool force = false) + { + long now = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); + + // Throttle non-critical updates to reduce overhead during large test runs + if (!force && (now - _lastPersistUnixMs) < MinPersistIntervalMs) + { + return; + } + + try + { + PersistedState snapshot; + lock (LockObj) + { + var jobs = Jobs.Values + .OrderByDescending(j => j.LastUpdateUnixMs) + .Take(MaxJobsToKeep) + .Select(j => new PersistedJob + { + job_id = j.JobId, + status = j.Status.ToString().ToLowerInvariant(), + mode = j.Mode, + started_unix_ms = j.StartedUnixMs, + finished_unix_ms = j.FinishedUnixMs, + last_update_unix_ms = j.LastUpdateUnixMs, + total_tests = j.TotalTests, + completed_tests = j.CompletedTests, + current_test_full_name = j.CurrentTestFullName, + current_test_started_unix_ms = j.CurrentTestStartedUnixMs, + last_finished_test_full_name = j.LastFinishedTestFullName, + last_finished_unix_ms = j.LastFinishedUnixMs, + failures_so_far = (j.FailuresSoFar ?? new List()).Take(FailureCap).ToList(), + error = j.Error + }) + .ToList(); + + snapshot = new PersistedState + { + current_job_id = _currentJobId, + jobs = jobs + }; + } + + SessionState.SetString(SessionKeyCurrentJobId, snapshot.current_job_id ?? string.Empty); + SessionState.SetString(SessionKeyJobs, JsonConvert.SerializeObject(snapshot)); + _lastPersistUnixMs = now; + } + catch (Exception ex) + { + McpLog.Warn($"[TestJobManager] Failed to persist SessionState: {ex.Message}"); + } + } + + public static string StartJob(TestMode mode, TestFilterOptions filterOptions = null) + { + string jobId = Guid.NewGuid().ToString("N"); + long started = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); + string modeStr = mode.ToString(); + + var job = new TestJob + { + JobId = jobId, + Status = TestJobStatus.Running, + Mode = modeStr, + StartedUnixMs = started, + FinishedUnixMs = null, + LastUpdateUnixMs = started, + TotalTests = null, + CompletedTests = 0, + CurrentTestFullName = null, + CurrentTestStartedUnixMs = null, + LastFinishedTestFullName = null, + LastFinishedUnixMs = null, + FailuresSoFar = new List(), + Error = null, + Result = null + }; + + // Single lock scope for check-and-set to avoid TOCTOU race + lock (LockObj) + { + if (!string.IsNullOrEmpty(_currentJobId)) + { + throw new InvalidOperationException("A Unity test run is already in progress."); + } + Jobs[jobId] = job; + _currentJobId = jobId; + } + PersistToSessionState(force: true); + + // Kick the run (must be called on main thread; our command handlers already run there). + Task task = MCPServiceLocator.Tests.RunTestsAsync(mode, filterOptions); + + void FinalizeJob(Action finalize) + { + // Ensure state mutation happens on main thread to avoid Unity API surprises. + EditorApplication.delayCall += () => + { + try { finalize(); } + catch (Exception ex) { McpLog.Error($"[TestJobManager] Finalize failed: {ex.Message}\n{ex.StackTrace}"); } + }; + } + + task.ContinueWith(t => + { + // NOTE: We now finalize jobs deterministically from the TestRunnerService RunFinished callback. + // This continuation is retained as a safety net in case RunFinished is not delivered. + FinalizeJob(() => FinalizeFromTask(jobId, t)); + }, TaskScheduler.Default); + + return jobId; + } + + public static void FinalizeCurrentJobFromRunFinished(TestRunResult resultPayload) + { + long now = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); + lock (LockObj) + { + if (string.IsNullOrEmpty(_currentJobId) || !Jobs.TryGetValue(_currentJobId, out var job)) + { + return; + } + + job.LastUpdateUnixMs = now; + job.FinishedUnixMs = now; + job.Status = resultPayload != null && resultPayload.Failed > 0 + ? TestJobStatus.Failed + : TestJobStatus.Succeeded; + job.Error = null; + job.Result = resultPayload; + job.CurrentTestFullName = null; + _currentJobId = null; + } + PersistToSessionState(force: true); + } + + public static void OnRunStarted(int? totalTests) + { + long now = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); + lock (LockObj) + { + if (string.IsNullOrEmpty(_currentJobId) || !Jobs.TryGetValue(_currentJobId, out var job)) + { + return; + } + + job.LastUpdateUnixMs = now; + job.TotalTests = totalTests; + job.CompletedTests = 0; + job.CurrentTestFullName = null; + job.CurrentTestStartedUnixMs = null; + job.LastFinishedTestFullName = null; + job.LastFinishedUnixMs = null; + job.FailuresSoFar ??= new List(); + job.FailuresSoFar.Clear(); + } + PersistToSessionState(force: true); + } + + public static void OnTestStarted(string testFullName) + { + if (string.IsNullOrWhiteSpace(testFullName)) + { + return; + } + + long now = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); + lock (LockObj) + { + if (string.IsNullOrEmpty(_currentJobId) || !Jobs.TryGetValue(_currentJobId, out var job)) + { + return; + } + + job.LastUpdateUnixMs = now; + job.CurrentTestFullName = testFullName; + job.CurrentTestStartedUnixMs = now; + } + PersistToSessionState(); + } + + public static void OnLeafTestFinished(string testFullName, bool isFailure, string message) + { + long now = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); + lock (LockObj) + { + if (string.IsNullOrEmpty(_currentJobId) || !Jobs.TryGetValue(_currentJobId, out var job)) + { + return; + } + + job.LastUpdateUnixMs = now; + job.CompletedTests = Math.Max(0, job.CompletedTests + 1); + job.LastFinishedTestFullName = testFullName; + job.LastFinishedUnixMs = now; + + if (isFailure) + { + job.FailuresSoFar ??= new List(); + if (job.FailuresSoFar.Count < FailureCap) + { + job.FailuresSoFar.Add(new TestJobFailure + { + FullName = testFullName, + Message = string.IsNullOrWhiteSpace(message) ? "Test failed" : message + }); + } + } + } + PersistToSessionState(); + } + + public static void OnRunFinished() + { + long now = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); + lock (LockObj) + { + if (string.IsNullOrEmpty(_currentJobId) || !Jobs.TryGetValue(_currentJobId, out var job)) + { + return; + } + + job.LastUpdateUnixMs = now; + job.CurrentTestFullName = null; + } + PersistToSessionState(force: true); + } + + public static TestJob GetJob(string jobId) + { + if (string.IsNullOrWhiteSpace(jobId)) + { + return null; + } + lock (LockObj) + { + return Jobs.TryGetValue(jobId, out var job) ? job : null; + } + } + + public static object ToSerializable(TestJob job, bool includeDetails, bool includeFailedTests) + { + if (job == null) + { + return null; + } + + object resultPayload = null; + if (job.Status == TestJobStatus.Succeeded && job.Result != null) + { + resultPayload = job.Result.ToSerializable(job.Mode, includeDetails, includeFailedTests); + } + + return new + { + job_id = job.JobId, + status = job.Status.ToString().ToLowerInvariant(), + mode = job.Mode, + started_unix_ms = job.StartedUnixMs, + finished_unix_ms = job.FinishedUnixMs, + last_update_unix_ms = job.LastUpdateUnixMs, + progress = new + { + completed = job.CompletedTests, + total = job.TotalTests, + current_test_full_name = job.CurrentTestFullName, + current_test_started_unix_ms = job.CurrentTestStartedUnixMs, + last_finished_test_full_name = job.LastFinishedTestFullName, + last_finished_unix_ms = job.LastFinishedUnixMs, + stuck_suspected = IsStuck(job), + editor_is_focused = InternalEditorUtility.isApplicationActive, + blocked_reason = GetBlockedReason(job), + failures_so_far = BuildFailuresPayload(job.FailuresSoFar), + failures_capped = (job.FailuresSoFar != null && job.FailuresSoFar.Count >= FailureCap) + }, + error = job.Error, + result = resultPayload + }; + } + + private static string GetBlockedReason(TestJob job) + { + if (job == null || job.Status != TestJobStatus.Running) + { + return null; + } + + if (!IsStuck(job)) + { + return null; + } + + // This matches the real-world symptom you observed: background Unity can get heavily throttled by OS/Editor. + if (!InternalEditorUtility.isApplicationActive) + { + return "editor_unfocused"; + } + + if (EditorApplication.isCompiling) + { + return "compiling"; + } + + if (EditorApplication.isUpdating) + { + return "asset_import"; + } + + return "unknown"; + } + + private static bool IsStuck(TestJob job) + { + if (job == null || job.Status != TestJobStatus.Running) + { + return false; + } + + if (string.IsNullOrWhiteSpace(job.CurrentTestFullName) || !job.CurrentTestStartedUnixMs.HasValue) + { + return false; + } + + long now = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); + return (now - job.CurrentTestStartedUnixMs.Value) > StuckThresholdMs; + } + + private static object[] BuildFailuresPayload(List failures) + { + if (failures == null || failures.Count == 0) + { + return Array.Empty(); + } + + var list = new object[failures.Count]; + for (int i = 0; i < failures.Count; i++) + { + var f = failures[i]; + list[i] = new { full_name = f?.FullName, message = f?.Message }; + } + return list; + } + + private static void FinalizeFromTask(string jobId, Task task) + { + lock (LockObj) + { + if (!Jobs.TryGetValue(jobId, out var existing)) + { + if (_currentJobId == jobId) _currentJobId = null; + return; + } + + // If RunFinished already finalized the job, do nothing. + if (existing.Status != TestJobStatus.Running) + { + if (_currentJobId == jobId) _currentJobId = null; + return; + } + + existing.LastUpdateUnixMs = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); + existing.FinishedUnixMs = existing.LastUpdateUnixMs; + + if (task.IsFaulted) + { + existing.Status = TestJobStatus.Failed; + existing.Error = task.Exception?.GetBaseException()?.Message ?? "Unknown test job failure"; + existing.Result = null; + } + else if (task.IsCanceled) + { + existing.Status = TestJobStatus.Failed; + existing.Error = "Test job canceled"; + existing.Result = null; + } + else + { + var result = task.Result; + existing.Status = result != null && result.Failed > 0 + ? TestJobStatus.Failed + : TestJobStatus.Succeeded; + existing.Error = null; + existing.Result = result; + } + + if (_currentJobId == jobId) + { + _currentJobId = null; + } + } + PersistToSessionState(force: true); + } + } +} + + diff --git a/MCPForUnity/Editor/Services/TestJobManager.cs.meta b/MCPForUnity/Editor/Services/TestJobManager.cs.meta new file mode 100644 index 0000000..0025599 --- /dev/null +++ b/MCPForUnity/Editor/Services/TestJobManager.cs.meta @@ -0,0 +1,13 @@ +fileFormatVersion: 2 +guid: 2d7a9b8c0e1f4a6b9c3d2e1f0a9b8c7d +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: + + diff --git a/MCPForUnity/Editor/Services/TestRunStatus.cs b/MCPForUnity/Editor/Services/TestRunStatus.cs new file mode 100644 index 0000000..da3ae6c --- /dev/null +++ b/MCPForUnity/Editor/Services/TestRunStatus.cs @@ -0,0 +1,62 @@ +using System; +using UnityEditor.TestTools.TestRunner.Api; + +namespace MCPForUnity.Editor.Services +{ + /// + /// Thread-safe, minimal shared status for Unity Test Runner execution. + /// Used by editor readiness snapshots so callers can avoid starting overlapping runs. + /// + internal static class TestRunStatus + { + private static readonly object LockObj = new(); + + private static bool _isRunning; + private static TestMode? _mode; + private static long? _startedUnixMs; + private static long? _finishedUnixMs; + + public static bool IsRunning + { + get { lock (LockObj) return _isRunning; } + } + + public static TestMode? Mode + { + get { lock (LockObj) return _mode; } + } + + public static long? StartedUnixMs + { + get { lock (LockObj) return _startedUnixMs; } + } + + public static long? FinishedUnixMs + { + get { lock (LockObj) return _finishedUnixMs; } + } + + public static void MarkStarted(TestMode mode) + { + lock (LockObj) + { + _isRunning = true; + _mode = mode; + _startedUnixMs = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); + _finishedUnixMs = null; + } + } + + public static void MarkFinished() + { + lock (LockObj) + { + _isRunning = false; + _finishedUnixMs = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); + _mode = null; + } + } + } +} + + diff --git a/MCPForUnity/Editor/Services/TestRunStatus.cs.meta b/MCPForUnity/Editor/Services/TestRunStatus.cs.meta new file mode 100644 index 0000000..8f499e0 --- /dev/null +++ b/MCPForUnity/Editor/Services/TestRunStatus.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: b3d140c288f6e4b6aa2b7e8181a09c1e +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/MCPForUnity/Editor/Services/TestRunnerNoThrottle.cs b/MCPForUnity/Editor/Services/TestRunnerNoThrottle.cs new file mode 100644 index 0000000..f379fd1 --- /dev/null +++ b/MCPForUnity/Editor/Services/TestRunnerNoThrottle.cs @@ -0,0 +1,139 @@ +// TestRunnerNoThrottle.cs +// Sets Unity Editor to "No Throttling" mode during test runs. +// This helps tests that don't trigger compilation run smoothly in the background. +// Note: Tests that trigger mid-run compilation may still stall due to OS-level throttling. + +using System; +using System.Reflection; +using MCPForUnity.Editor.Helpers; +using UnityEditor; +using UnityEditor.TestTools.TestRunner.Api; +using UnityEngine; + +namespace MCPForUnity.Editor.Services +{ + /// + /// Automatically sets the editor to "No Throttling" mode during test runs. + /// + /// This helps prevent background stalls for normal tests. However, tests that trigger + /// script compilation mid-run may still stall because: + /// - Internal Unity coroutine waits rely on editor ticks + /// - OS-level throttling affects the main thread when Unity is backgrounded + /// - No amount of internal nudging can overcome OS thread scheduling + /// + /// The MCP workflow is unaffected because socket messages provide external stimulus + /// that wakes Unity's main thread. + /// + [InitializeOnLoad] + public static class TestRunnerNoThrottle + { + private const string ApplicationIdleTimeKey = "ApplicationIdleTime"; + private const string InteractionModeKey = "InteractionMode"; + + // SessionState keys to persist across domain reload + private const string SessionKey_TestRunActive = "TestRunnerNoThrottle_TestRunActive"; + private const string SessionKey_PrevIdleTime = "TestRunnerNoThrottle_PrevIdleTime"; + private const string SessionKey_PrevInteractionMode = "TestRunnerNoThrottle_PrevInteractionMode"; + private const string SessionKey_SettingsCaptured = "TestRunnerNoThrottle_SettingsCaptured"; + + // Keep reference to avoid GC and set HideFlags to avoid serialization issues + private static TestRunnerApi _api; + + static TestRunnerNoThrottle() + { + try + { + _api = ScriptableObject.CreateInstance(); + _api.hideFlags = HideFlags.HideAndDontSave; + _api.RegisterCallbacks(new TestCallbacks()); + + // Check if recovering from domain reload during an active test run + if (IsTestRunActive()) + { + McpLog.Info("[TestRunnerNoThrottle] Recovered from domain reload - reapplying No Throttling."); + ApplyNoThrottling(); + } + } + catch (Exception e) + { + McpLog.Warn($"[TestRunnerNoThrottle] Failed to register callbacks: {e}"); + } + } + + #region State Persistence + + private static bool IsTestRunActive() => SessionState.GetBool(SessionKey_TestRunActive, false); + private static void SetTestRunActive(bool active) => SessionState.SetBool(SessionKey_TestRunActive, active); + private static bool AreSettingsCaptured() => SessionState.GetBool(SessionKey_SettingsCaptured, false); + private static void SetSettingsCaptured(bool captured) => SessionState.SetBool(SessionKey_SettingsCaptured, captured); + private static int GetPrevIdleTime() => SessionState.GetInt(SessionKey_PrevIdleTime, 4); + private static void SetPrevIdleTime(int value) => SessionState.SetInt(SessionKey_PrevIdleTime, value); + private static int GetPrevInteractionMode() => SessionState.GetInt(SessionKey_PrevInteractionMode, 0); + private static void SetPrevInteractionMode(int value) => SessionState.SetInt(SessionKey_PrevInteractionMode, value); + + #endregion + + private static void ApplyNoThrottling() + { + if (!AreSettingsCaptured()) + { + SetPrevIdleTime(EditorPrefs.GetInt(ApplicationIdleTimeKey, 4)); + SetPrevInteractionMode(EditorPrefs.GetInt(InteractionModeKey, 0)); + SetSettingsCaptured(true); + } + + // 0ms idle + InteractionMode=1 (No Throttling) + EditorPrefs.SetInt(ApplicationIdleTimeKey, 0); + EditorPrefs.SetInt(InteractionModeKey, 1); + + ForceEditorToApplyInteractionPrefs(); + McpLog.Info("[TestRunnerNoThrottle] Applied No Throttling for test run."); + } + + private static void RestoreThrottling() + { + if (!AreSettingsCaptured()) return; + + EditorPrefs.SetInt(ApplicationIdleTimeKey, GetPrevIdleTime()); + EditorPrefs.SetInt(InteractionModeKey, GetPrevInteractionMode()); + ForceEditorToApplyInteractionPrefs(); + + SetSettingsCaptured(false); + SetTestRunActive(false); + McpLog.Info("[TestRunnerNoThrottle] Restored Interaction Mode after test run."); + } + + private static void ForceEditorToApplyInteractionPrefs() + { + try + { + var method = typeof(EditorApplication).GetMethod( + "UpdateInteractionModeSettings", + BindingFlags.Static | BindingFlags.NonPublic + ); + method?.Invoke(null, null); + } + catch + { + // Ignore reflection errors + } + } + + private sealed class TestCallbacks : ICallbacks + { + public void RunStarted(ITestAdaptor testsToRun) + { + SetTestRunActive(true); + ApplyNoThrottling(); + } + + public void RunFinished(ITestResultAdaptor result) + { + RestoreThrottling(); + } + + public void TestStarted(ITestAdaptor test) { } + public void TestFinished(ITestResultAdaptor result) { } + } + } +} diff --git a/MCPForUnity/Editor/Services/TestRunnerNoThrottle.cs.meta b/MCPForUnity/Editor/Services/TestRunnerNoThrottle.cs.meta new file mode 100644 index 0000000..8e9a8d4 --- /dev/null +++ b/MCPForUnity/Editor/Services/TestRunnerNoThrottle.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 07a60b029782d464a9506fa520d2a8c8 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/MCPForUnity/Editor/Services/TestRunnerService.cs b/MCPForUnity/Editor/Services/TestRunnerService.cs index ebb92a2..baa8957 100644 --- a/MCPForUnity/Editor/Services/TestRunnerService.cs +++ b/MCPForUnity/Editor/Services/TestRunnerService.cs @@ -93,6 +93,8 @@ namespace MCPForUnity.Editor.Services _leafResults.Clear(); _runCompletionSource = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + // Mark running immediately so readiness snapshots reflect the busy state even before callbacks fire. + TestRunStatus.MarkStarted(mode); var filter = new Filter { @@ -115,6 +117,8 @@ namespace MCPForUnity.Editor.Services } catch { + // Ensure the status is cleared if we failed to start the run. + TestRunStatus.MarkFinished(); if (adjustedPlayModeOptions) { RestoreEnterPlayModeOptions(originalEnterPlayModeOptionsEnabled, originalEnterPlayModeOptions); @@ -163,6 +167,20 @@ namespace MCPForUnity.Editor.Services public void RunStarted(ITestAdaptor testsToRun) { _leafResults.Clear(); + try + { + // Best-effort progress info for async polling (avoid heavy payloads). + int? total = null; + if (testsToRun != null) + { + total = CountLeafTests(testsToRun); + } + TestJobManager.OnRunStarted(total); + } + catch + { + TestJobManager.OnRunStarted(null); + } } public void RunFinished(ITestResultAdaptor result) @@ -175,11 +193,27 @@ namespace MCPForUnity.Editor.Services var payload = TestRunResult.Create(result, _leafResults); _runCompletionSource.TrySetResult(payload); _runCompletionSource = null; + TestRunStatus.MarkFinished(); + TestJobManager.OnRunFinished(); + TestJobManager.FinalizeCurrentJobFromRunFinished(payload); } public void TestStarted(ITestAdaptor test) { - // No-op + try + { + // Prefer FullName for uniqueness; fall back to Name. + string fullName = test?.FullName; + if (string.IsNullOrWhiteSpace(fullName)) + { + fullName = test?.Name; + } + TestJobManager.OnTestStarted(fullName); + } + catch + { + // ignore + } } public void TestFinished(ITestResultAdaptor result) @@ -192,11 +226,72 @@ namespace MCPForUnity.Editor.Services if (!result.HasChildren) { _leafResults.Add(result); + try + { + string fullName = result.Test?.FullName; + if (string.IsNullOrWhiteSpace(fullName)) + { + fullName = result.Test?.Name; + } + + bool isFailure = false; + string message = null; + try + { + // NUnit outcomes are strings in the adaptor; keep it simple. + string outcome = result.ResultState; + if (!string.IsNullOrWhiteSpace(outcome)) + { + var o = outcome.Trim().ToLowerInvariant(); + isFailure = o.Contains("failed") || o.Contains("error"); + } + message = result.Message; + } + catch + { + // ignore adaptor quirks + } + + TestJobManager.OnLeafTestFinished(fullName, isFailure, message); + } + catch + { + // ignore + } } } #endregion + private static int CountLeafTests(ITestAdaptor node) + { + if (node == null) + { + return 0; + } + + if (!node.HasChildren) + { + return 1; + } + + int total = 0; + try + { + foreach (var child in node.Children) + { + total += CountLeafTests(child); + } + } + catch + { + // If Unity changes the adaptor behavior, treat it as "unknown total". + return 0; + } + + return total; + } + private static bool EnsurePlayModeRunsWithoutDomainReload( out bool originalEnterPlayModeOptionsEnabled, out EnterPlayModeOptions originalEnterPlayModeOptions) diff --git a/MCPForUnity/Editor/Tools/GetTestJob.cs b/MCPForUnity/Editor/Tools/GetTestJob.cs new file mode 100644 index 0000000..8983155 --- /dev/null +++ b/MCPForUnity/Editor/Tools/GetTestJob.cs @@ -0,0 +1,54 @@ +using System; +using MCPForUnity.Editor.Helpers; +using MCPForUnity.Editor.Services; +using Newtonsoft.Json.Linq; + +namespace MCPForUnity.Editor.Tools +{ + /// + /// Poll a previously started async test job by job_id. + /// + [McpForUnityTool("get_test_job", AutoRegister = false)] + public static class GetTestJob + { + public static object HandleCommand(JObject @params) + { + string jobId = @params?["job_id"]?.ToString() ?? @params?["jobId"]?.ToString(); + if (string.IsNullOrWhiteSpace(jobId)) + { + return new ErrorResponse("Missing required parameter 'job_id'."); + } + + bool includeDetails = false; + bool includeFailedTests = false; + try + { + var includeDetailsToken = @params?["includeDetails"]; + if (includeDetailsToken != null && bool.TryParse(includeDetailsToken.ToString(), out var parsedIncludeDetails)) + { + includeDetails = parsedIncludeDetails; + } + var includeFailedTestsToken = @params?["includeFailedTests"]; + if (includeFailedTestsToken != null && bool.TryParse(includeFailedTestsToken.ToString(), out var parsedIncludeFailedTests)) + { + includeFailedTests = parsedIncludeFailedTests; + } + } + catch + { + // ignore parse failures + } + + var job = TestJobManager.GetJob(jobId); + if (job == null) + { + return new ErrorResponse("Unknown job_id."); + } + + var payload = TestJobManager.ToSerializable(job, includeDetails, includeFailedTests); + return new SuccessResponse("Test job status retrieved.", payload); + } + } +} + + diff --git a/MCPForUnity/Editor/Tools/GetTestJob.cs.meta b/MCPForUnity/Editor/Tools/GetTestJob.cs.meta new file mode 100644 index 0000000..d0b52eb --- /dev/null +++ b/MCPForUnity/Editor/Tools/GetTestJob.cs.meta @@ -0,0 +1,13 @@ +fileFormatVersion: 2 +guid: 7f92c2b67a2c4b5c9d1a3c0e6f9b2d10 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: + + diff --git a/MCPForUnity/Editor/Tools/ManageAsset.cs b/MCPForUnity/Editor/Tools/ManageAsset.cs index eecbcac..a4d1119 100644 --- a/MCPForUnity/Editor/Tools/ManageAsset.cs +++ b/MCPForUnity/Editor/Tools/ManageAsset.cs @@ -180,7 +180,7 @@ namespace MCPForUnity.Editor.Tools if (!Directory.Exists(Path.Combine(Directory.GetCurrentDirectory(), directory))) { Directory.CreateDirectory(Path.Combine(Directory.GetCurrentDirectory(), directory)); - AssetDatabase.Refresh(); // Make sure Unity knows about the new folder + AssetDatabase.Refresh(ImportAssetOptions.ForceSynchronousImport); // Make sure Unity knows about the new folder } if (AssetExists(fullPath)) @@ -869,7 +869,7 @@ namespace MCPForUnity.Editor.Tools if (!Directory.Exists(fullDirPath)) { Directory.CreateDirectory(fullDirPath); - AssetDatabase.Refresh(); // Let Unity know about the new folder + AssetDatabase.Refresh(ImportAssetOptions.ForceSynchronousImport); // Let Unity know about the new folder } } diff --git a/MCPForUnity/Editor/Tools/ManageGameObject.cs b/MCPForUnity/Editor/Tools/ManageGameObject.cs index d7ebfa3..f519a44 100644 --- a/MCPForUnity/Editor/Tools/ManageGameObject.cs +++ b/MCPForUnity/Editor/Tools/ManageGameObject.cs @@ -590,7 +590,7 @@ namespace MCPForUnity.Editor.Tools ) { System.IO.Directory.CreateDirectory(directoryPath); - AssetDatabase.Refresh(); // Refresh asset database to recognize the new folder + AssetDatabase.Refresh(ImportAssetOptions.ForceSynchronousImport); // Refresh asset database to recognize the new folder Debug.Log( $"[ManageGameObject.Create] Created directory for prefab: {directoryPath}" ); diff --git a/MCPForUnity/Editor/Tools/ManageScene.cs b/MCPForUnity/Editor/Tools/ManageScene.cs index 2c10f45..488716c 100644 --- a/MCPForUnity/Editor/Tools/ManageScene.cs +++ b/MCPForUnity/Editor/Tools/ManageScene.cs @@ -219,7 +219,7 @@ namespace MCPForUnity.Editor.Tools if (saved) { - AssetDatabase.Refresh(); // Ensure Unity sees the new scene file + AssetDatabase.Refresh(ImportAssetOptions.ForceSynchronousImport); // Ensure Unity sees the new scene file return new SuccessResponse( $"Scene '{Path.GetFileName(relativePath)}' created successfully at '{relativePath}'.", new { path = relativePath } @@ -362,7 +362,7 @@ namespace MCPForUnity.Editor.Tools if (saved) { - AssetDatabase.Refresh(); + AssetDatabase.Refresh(ImportAssetOptions.ForceSynchronousImport); return new SuccessResponse( $"Scene '{currentScene.name}' saved successfully to '{finalPath}'.", new { path = finalPath, name = currentScene.name } @@ -408,7 +408,7 @@ namespace MCPForUnity.Editor.Tools result = ScreenshotUtility.CaptureFromCameraToAssetsFolder(cam, fileName, resolvedSuperSize, ensureUniqueFileName: true); } - AssetDatabase.Refresh(); + AssetDatabase.Refresh(ImportAssetOptions.ForceSynchronousImport); string message = $"Screenshot captured to '{result.AssetsRelativePath}' (full: {result.FullPath})."; diff --git a/MCPForUnity/Editor/Tools/ManageScript.cs b/MCPForUnity/Editor/Tools/ManageScript.cs index 03da19d..e1ae813 100644 --- a/MCPForUnity/Editor/Tools/ManageScript.cs +++ b/MCPForUnity/Editor/Tools/ManageScript.cs @@ -978,7 +978,7 @@ namespace MCPForUnity.Editor.Tools bool deleted = AssetDatabase.MoveAssetToTrash(relativePath); if (deleted) { - AssetDatabase.Refresh(); + AssetDatabase.Refresh(ImportAssetOptions.ForceSynchronousImport); return new SuccessResponse( $"Script '{Path.GetFileName(relativePath)}' moved to trash successfully.", new { deleted = true } diff --git a/MCPForUnity/Editor/Tools/ManageShader.cs b/MCPForUnity/Editor/Tools/ManageShader.cs index 2b61806..9edd2d9 100644 --- a/MCPForUnity/Editor/Tools/ManageShader.cs +++ b/MCPForUnity/Editor/Tools/ManageShader.cs @@ -94,7 +94,7 @@ namespace MCPForUnity.Editor.Tools { Directory.CreateDirectory(fullPathDir); // Refresh AssetDatabase to recognize new folders - AssetDatabase.Refresh(); + AssetDatabase.Refresh(ImportAssetOptions.ForceSynchronousImport); } } catch (Exception e) @@ -174,7 +174,7 @@ namespace MCPForUnity.Editor.Tools { File.WriteAllText(fullPath, contents, new System.Text.UTF8Encoding(false)); AssetDatabase.ImportAsset(relativePath); - AssetDatabase.Refresh(); // Ensure Unity recognizes the new shader + AssetDatabase.Refresh(ImportAssetOptions.ForceSynchronousImport); // Ensure Unity recognizes the new shader return new SuccessResponse( $"Shader '{name}.shader' created successfully at '{relativePath}'.", new { path = relativePath } @@ -242,7 +242,7 @@ namespace MCPForUnity.Editor.Tools { File.WriteAllText(fullPath, contents, new System.Text.UTF8Encoding(false)); AssetDatabase.ImportAsset(relativePath); - AssetDatabase.Refresh(); + AssetDatabase.Refresh(ImportAssetOptions.ForceSynchronousImport); return new SuccessResponse( $"Shader '{Path.GetFileName(relativePath)}' updated successfully.", new { path = relativePath } diff --git a/MCPForUnity/Editor/Tools/Prefabs/ManagePrefabs.cs b/MCPForUnity/Editor/Tools/Prefabs/ManagePrefabs.cs index 18b4ae0..39ed057 100644 --- a/MCPForUnity/Editor/Tools/Prefabs/ManagePrefabs.cs +++ b/MCPForUnity/Editor/Tools/Prefabs/ManagePrefabs.cs @@ -223,7 +223,7 @@ namespace MCPForUnity.Editor.Tools.Prefabs if (!Directory.Exists(fullDirectory)) { Directory.CreateDirectory(fullDirectory); - AssetDatabase.Refresh(); + AssetDatabase.Refresh(ImportAssetOptions.ForceSynchronousImport); } } diff --git a/MCPForUnity/Editor/Tools/RefreshUnity.cs b/MCPForUnity/Editor/Tools/RefreshUnity.cs new file mode 100644 index 0000000..7dd2f0d --- /dev/null +++ b/MCPForUnity/Editor/Tools/RefreshUnity.cs @@ -0,0 +1,175 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using MCPForUnity.Editor.Helpers; +using MCPForUnity.Editor.Services; +using Newtonsoft.Json.Linq; +using UnityEditor; +using UnityEditor.Compilation; + +namespace MCPForUnity.Editor.Tools +{ + /// + /// Explicitly refreshes Unity's asset database and optionally requests a script compilation. + /// This is side-effectful and should be treated as a tool. + /// + [McpForUnityTool("refresh_unity", AutoRegister = false)] + public static class RefreshUnity + { + private const int DefaultWaitTimeoutSeconds = 60; + + public static async Task HandleCommand(JObject @params) + { + string mode = @params?["mode"]?.ToString() ?? "if_dirty"; + string scope = @params?["scope"]?.ToString() ?? "all"; + string compile = @params?["compile"]?.ToString() ?? "none"; + bool waitForReady = false; + + try + { + var waitToken = @params?["wait_for_ready"]; + if (waitToken != null && bool.TryParse(waitToken.ToString(), out var parsed)) + { + waitForReady = parsed; + } + } + catch + { + // ignore parse failures + } + + if (TestRunStatus.IsRunning) + { + return new ErrorResponse("tests_running", new + { + reason = "tests_running", + retry_after_ms = 5000 + }); + } + + bool refreshTriggered = false; + bool compileRequested = false; + + try + { + // Best-effort semantics: if_dirty currently behaves like force unless future dirty signals are added. + bool shouldRefresh = string.Equals(mode, "force", StringComparison.OrdinalIgnoreCase) + || string.Equals(mode, "if_dirty", StringComparison.OrdinalIgnoreCase); + + if (shouldRefresh) + { + if (string.Equals(scope, "scripts", StringComparison.OrdinalIgnoreCase)) + { + // For scripts, requesting compilation is usually the meaningful action. + // We avoid a heavyweight full refresh by default. + } + else + { + AssetDatabase.Refresh(ImportAssetOptions.ForceUpdate | ImportAssetOptions.ForceSynchronousImport); + refreshTriggered = true; + } + } + + if (string.Equals(compile, "request", StringComparison.OrdinalIgnoreCase)) + { + CompilationPipeline.RequestScriptCompilation(); + compileRequested = true; + } + + if (string.Equals(scope, "all", StringComparison.OrdinalIgnoreCase) && !refreshTriggered) + { + // If the caller asked for "all" and we skipped refresh above (e.g., scripts-only path), + // do a lightweight refresh now. Use ForceSynchronousImport to ensure the refresh + // completes before returning, preventing stalls when Unity is backgrounded. + AssetDatabase.Refresh(ImportAssetOptions.ForceSynchronousImport); + refreshTriggered = true; + } + } + catch (Exception ex) + { + return new ErrorResponse($"refresh_failed: {ex.Message}"); + } + + if (waitForReady) + { + try + { + await WaitForUnityReadyAsync(TimeSpan.FromSeconds(DefaultWaitTimeoutSeconds)).ConfigureAwait(true); + } + catch (TimeoutException) + { + return new ErrorResponse("refresh_timeout_waiting_for_ready", new + { + refresh_triggered = refreshTriggered, + compile_requested = compileRequested, + resulting_state = "unknown", + }); + } + catch (Exception ex) + { + return new ErrorResponse($"refresh_wait_failed: {ex.Message}"); + } + } + + string resultingState = EditorApplication.isCompiling + ? "compiling" + : (EditorApplication.isUpdating ? "asset_import" : "idle"); + + return new SuccessResponse("Refresh requested.", new + { + refresh_triggered = refreshTriggered, + compile_requested = compileRequested, + resulting_state = resultingState, + hint = waitForReady + ? "Unity refresh completed; editor should be ready." + : "If Unity enters compilation/domain reload, poll editor_state until ready_for_tools is true." + }); + } + + private static Task WaitForUnityReadyAsync(TimeSpan timeout) + { + var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + var start = DateTime.UtcNow; + + void Tick() + { + try + { + if (tcs.Task.IsCompleted) + { + EditorApplication.update -= Tick; + return; + } + + if ((DateTime.UtcNow - start) > timeout) + { + EditorApplication.update -= Tick; + tcs.TrySetException(new TimeoutException()); + return; + } + + if (!EditorApplication.isCompiling + && !EditorApplication.isUpdating + && !TestRunStatus.IsRunning + && !EditorApplication.isPlayingOrWillChangePlaymode) + { + EditorApplication.update -= Tick; + tcs.TrySetResult(true); + } + } + catch (Exception ex) + { + EditorApplication.update -= Tick; + tcs.TrySetException(ex); + } + } + + EditorApplication.update += Tick; + // Nudge Unity to pump once in case update is throttled. + try { EditorApplication.QueuePlayerLoopUpdate(); } catch { } + return tcs.Task; + } + } +} + + diff --git a/MCPForUnity/Editor/Tools/RefreshUnity.cs.meta b/MCPForUnity/Editor/Tools/RefreshUnity.cs.meta new file mode 100644 index 0000000..a2e3846 --- /dev/null +++ b/MCPForUnity/Editor/Tools/RefreshUnity.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: c2c02170faca940d09c813706493ecb3 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/MCPForUnity/Editor/Tools/RunTestsAsync.cs b/MCPForUnity/Editor/Tools/RunTestsAsync.cs new file mode 100644 index 0000000..5550c1f --- /dev/null +++ b/MCPForUnity/Editor/Tools/RunTestsAsync.cs @@ -0,0 +1,128 @@ +using System; +using System.Linq; +using System.Threading.Tasks; +using MCPForUnity.Editor.Helpers; +using MCPForUnity.Editor.Resources.Tests; +using MCPForUnity.Editor.Services; +using Newtonsoft.Json.Linq; +using UnityEditor.TestTools.TestRunner.Api; + +namespace MCPForUnity.Editor.Tools +{ + /// + /// Starts a Unity Test Runner run asynchronously and returns a job id immediately. + /// Use get_test_job(job_id) to poll status/results. + /// + [McpForUnityTool("run_tests_async", AutoRegister = false)] + public static class RunTestsAsync + { + public static Task HandleCommand(JObject @params) + { + try + { + string modeStr = @params?["mode"]?.ToString(); + if (string.IsNullOrWhiteSpace(modeStr)) + { + modeStr = "EditMode"; + } + + if (!ModeParser.TryParse(modeStr, out var parsedMode, out var parseError)) + { + return Task.FromResult(new ErrorResponse(parseError)); + } + + bool includeDetails = false; + bool includeFailedTests = false; + try + { + var includeDetailsToken = @params?["includeDetails"]; + if (includeDetailsToken != null && bool.TryParse(includeDetailsToken.ToString(), out var parsedIncludeDetails)) + { + includeDetails = parsedIncludeDetails; + } + + var includeFailedTestsToken = @params?["includeFailedTests"]; + if (includeFailedTestsToken != null && bool.TryParse(includeFailedTestsToken.ToString(), out var parsedIncludeFailedTests)) + { + includeFailedTests = parsedIncludeFailedTests; + } + } + catch + { + // ignore parse failures + } + + var filterOptions = GetFilterOptions(@params); + string jobId = TestJobManager.StartJob(parsedMode.Value, filterOptions); + + return Task.FromResult(new SuccessResponse("Test job started.", new + { + job_id = jobId, + status = "running", + mode = parsedMode.Value.ToString(), + include_details = includeDetails, + include_failed_tests = includeFailedTests + })); + } + catch (Exception ex) + { + // Normalize the already-running case to a stable error token. + if (ex.Message != null && ex.Message.IndexOf("already in progress", StringComparison.OrdinalIgnoreCase) >= 0) + { + return Task.FromResult(new ErrorResponse("tests_running", new { reason = "tests_running", retry_after_ms = 5000 })); + } + return Task.FromResult(new ErrorResponse($"Failed to start test job: {ex.Message}")); + } + } + + private static TestFilterOptions GetFilterOptions(JObject @params) + { + if (@params == null) + { + return null; + } + + string[] ParseStringArray(string key) + { + var token = @params[key]; + if (token == null) return null; + if (token.Type == JTokenType.String) + { + var value = token.ToString(); + return string.IsNullOrWhiteSpace(value) ? null : new[] { value }; + } + if (token.Type == JTokenType.Array) + { + var array = token as JArray; + if (array == null || array.Count == 0) return null; + var values = array + .Values() + .Where(s => !string.IsNullOrWhiteSpace(s)) + .ToArray(); + return values.Length > 0 ? values : null; + } + return null; + } + + var testNames = ParseStringArray("testNames"); + var groupNames = ParseStringArray("groupNames"); + var categoryNames = ParseStringArray("categoryNames"); + var assemblyNames = ParseStringArray("assemblyNames"); + + if (testNames == null && groupNames == null && categoryNames == null && assemblyNames == null) + { + return null; + } + + return new TestFilterOptions + { + TestNames = testNames, + GroupNames = groupNames, + CategoryNames = categoryNames, + AssemblyNames = assemblyNames + }; + } + } +} + + diff --git a/MCPForUnity/Editor/Tools/RunTestsAsync.cs.meta b/MCPForUnity/Editor/Tools/RunTestsAsync.cs.meta new file mode 100644 index 0000000..8b23e7f --- /dev/null +++ b/MCPForUnity/Editor/Tools/RunTestsAsync.cs.meta @@ -0,0 +1,13 @@ +fileFormatVersion: 2 +guid: 5cc0c41b1a8b4e0e9d0f1f8b1d7d2a9c +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: + + diff --git a/MCPForUnity/Editor/Windows/Components/Connection/McpConnectionSection.cs b/MCPForUnity/Editor/Windows/Components/Connection/McpConnectionSection.cs index d1073ef..eb709bb 100644 --- a/MCPForUnity/Editor/Windows/Components/Connection/McpConnectionSection.cs +++ b/MCPForUnity/Editor/Windows/Components/Connection/McpConnectionSection.cs @@ -627,10 +627,20 @@ namespace MCPForUnity.Editor.Windows.Components.Connection { // Wait until the HTTP server is actually accepting connections to reduce transient "unable to connect then recovers" // behavior (session start attempts can race the server startup). - bool serverReady = await WaitForHttpServerAcceptingConnectionsAsync(TimeSpan.FromSeconds(10)); + // Dev mode uses --no-cache --refresh which causes uvx to rebuild the package, taking significantly longer. + bool devModeEnabled = false; + try { devModeEnabled = EditorPrefs.GetBool(EditorPrefKeys.DevModeForceServerRefresh, false); } catch { } + var startupTimeout = devModeEnabled ? TimeSpan.FromSeconds(45) : TimeSpan.FromSeconds(10); + + if (devModeEnabled) + { + McpLog.Info("Dev mode enabled: server startup may take longer while uvx rebuilds the package..."); + } + + bool serverReady = await WaitForHttpServerAcceptingConnectionsAsync(startupTimeout); if (!serverReady) { - McpLog.Warn("HTTP server did not become reachable within the expected startup window; will still attempt to start the session."); + McpLog.Warn($"HTTP server did not become reachable within {startupTimeout.TotalSeconds}s; will still attempt to start the session."); } for (int attempt = 0; attempt < maxAttempts; attempt++) diff --git a/README.md b/README.md index 62ab224..08d845d 100644 --- a/README.md +++ b/README.md @@ -16,9 +16,9 @@ **Create your Unity apps with LLMs!** -MCP for Unity acts as a bridge, allowing AI assistants (like Claude, Cursor) to interact directly with your Unity Editor via a local **MCP (Model Context Protocol) Client**. Give your LLM tools to manage assets, control scenes, edit scripts, and automate tasks within Unity. +MCP for Unity acts as a bridge, allowing AI assistants (Claude, Cursor, Antigravity, VS Code, etc) to interact directly with your Unity Editor via a local **MCP (Model Context Protocol) Client**. Give your LLM tools to manage assets, control scenes, edit scripts, and automate tasks within Unity. -MCP for Unity screenshot +MCP for Unity screenshot --- @@ -51,7 +51,9 @@ MCP for Unity acts as a bridge, allowing AI assistants (like Claude, Cursor) to * `manage_scriptable_object`: Creates and modifies ScriptableObject assets using Unity SerializedObject property paths. * `manage_shader`: Performs shader CRUD operations (create, read, modify, delete). * `read_console`: Gets messages from or clears the console. -* `run_tests`: Runs tests in the Unity Editor. +* `run_tests_async`: Starts tests asynchronously and returns a job_id for polling (preferred). +* `get_test_job`: Polls an async test job for progress and results. +* `run_tests`: Runs tests synchronously (blocks until complete; prefer `run_tests_async` for long suites). * `execute_custom_tool`: Execute a project-scoped custom tool registered by Unity. * `execute_menu_item`: Executes Unity Editor menu items (e.g., "File/Save Project"). * `set_active_instance`: Routes subsequent tool calls to a specific Unity instance (when multiple are running). Requires the exact `Name@hash` from `unity_instances`. @@ -152,7 +154,7 @@ MCP for Unity connects your tools using two components: **Need a stable/fixed version?** Use a tagged URL instead (updates require uninstalling and re-installing): ``` -https://github.com/CoplayDev/unity-mcp.git?path=/MCPForUnity#v8.2.1 +https://github.com/CoplayDev/unity-mcp.git?path=/MCPForUnity#v8.6.0 ``` #### To install via OpenUPM @@ -168,8 +170,8 @@ https://github.com/CoplayDev/unity-mcp.git?path=/MCPForUnity#v8.2.1 HTTP transport is enabled out of the box. The Unity window can launch the FastMCP server for you: 1. Open `Window > MCP for Unity`. -2. Make sure the **Transport** dropdown is set to `HTTP` (default) and the **HTTP URL** is what you want (defaults to `http://localhost:8080`). -3. Click **Start Local HTTP Server**. Unity spawns a new operating-system terminal running `uv ... server.py --transport http`. +2. Make sure the **Transport** dropdown is set to `HTTP Local` (default) and the **HTTP URL** is what you want (defaults to `http://localhost:8080`). +3. Click **Start Server**. Unity spawns a new operating-system terminal running `uv ... server.py --transport http`. 4. Keep that terminal window open while you work; closing it stops the server. Use the **Stop Session** button in the Unity window if you need to tear it down cleanly. > Prefer stdio? Change the transport dropdown to `Stdio` and Unity will fall back to the embedded TCP bridge instead of launching the HTTP server. @@ -179,7 +181,7 @@ HTTP transport is enabled out of the box. The Unity window can launch the FastMC You can also start the server yourself from a terminal—useful for CI or when you want to see raw logs: ```bash -uvx --from "git+https://github.com/CoplayDev/unity-mcp@v8.1.0#subdirectory=Server" mcp-for-unity --transport http --http-url http://localhost:8080 +uvx --from "git+https://github.com/CoplayDev/unity-mcp@v8.6.0#subdirectory=Server" mcp-for-unity --transport http --http-url http://localhost:8080 ``` Keep the process running while clients are connected. @@ -407,7 +409,7 @@ Your privacy matters to us. All telemetry is optional and designed to respect yo - Check the status window: Window > MCP for Unity. - Restart Unity. - **MCP Client Not Connecting / Server Not Starting:** - - Make sure the local HTTP server is running (Window > MCP for Unity > Start Local HTTP Server). Keep the spawned terminal window open. + - Make sure the local HTTP server is running (Window > MCP for Unity > Start Server). Keep the spawned terminal window open. - **Verify Server Path:** Double-check the --directory path in your MCP Client's JSON config. It must exactly match the installation location: - **Windows:** `%USERPROFILE%\AppData\Local\UnityMCP\UnityMcpServer\src` - **macOS:** `~/Library/AppSupport/UnityMCP/UnityMcpServer\src` diff --git a/Server/src/services/resources/editor_state_v2.py b/Server/src/services/resources/editor_state_v2.py new file mode 100644 index 0000000..212d114 --- /dev/null +++ b/Server/src/services/resources/editor_state_v2.py @@ -0,0 +1,270 @@ +import time +import os +from typing import Any + +from fastmcp import Context + +from models import MCPResponse +from services.registry import mcp_for_unity_resource +from services.tools import get_unity_instance_from_context +import transport.unity_transport as unity_transport +from transport.legacy.unity_connection import async_send_command_with_retry +from services.state.external_changes_scanner import external_changes_scanner + + +def _now_unix_ms() -> int: + return int(time.time() * 1000) + + +def _in_pytest() -> bool: + # Avoid instance-discovery side effects during the Python integration test suite. + return bool(os.environ.get("PYTEST_CURRENT_TEST")) + + +async def _infer_single_instance_id(ctx: Context) -> str | None: + """ + Best-effort: if exactly one Unity instance is connected, return its Name@hash id. + This makes editor_state outputs self-describing even when no explicit active instance is set. + """ + if _in_pytest(): + return None + + try: + transport = unity_transport._current_transport() + except Exception: + transport = None + + if transport == "http": + # HTTP/WebSocket transport: derive from PluginHub sessions. + try: + from transport.plugin_hub import PluginHub + + sessions_data = await PluginHub.get_sessions() + sessions = sessions_data.sessions if hasattr(sessions_data, "sessions") else {} + if isinstance(sessions, dict) and len(sessions) == 1: + session = next(iter(sessions.values())) + project = getattr(session, "project", None) + project_hash = getattr(session, "hash", None) + if project and project_hash: + return f"{project}@{project_hash}" + except Exception: + return None + return None + + # Stdio/TCP transport: derive from connection pool discovery. + try: + from transport.legacy.unity_connection import get_unity_connection_pool + + pool = get_unity_connection_pool() + instances = pool.discover_all_instances(force_refresh=False) + if isinstance(instances, list) and len(instances) == 1: + inst = instances[0] + inst_id = getattr(inst, "id", None) + return str(inst_id) if inst_id else None + except Exception: + return None + return None + + +def _build_v2_from_legacy(legacy: dict[str, Any]) -> dict[str, Any]: + """ + Best-effort mapping from legacy get_editor_state payload into the v2 contract. + Legacy shape (Unity): {isPlaying,isPaused,isCompiling,isUpdating,timeSinceStartup,...} + """ + now_ms = _now_unix_ms() + # legacy may arrive already wrapped as MCPResponse-like {success,data:{...}} + state = legacy.get("data") if isinstance(legacy.get("data"), dict) else {} + + return { + "schema_version": "unity-mcp/editor_state@2", + "observed_at_unix_ms": now_ms, + "sequence": 0, + "unity": { + "instance_id": None, + "unity_version": None, + "project_id": None, + "platform": None, + "is_batch_mode": None, + }, + "editor": { + "is_focused": None, + "play_mode": { + "is_playing": bool(state.get("isPlaying", False)), + "is_paused": bool(state.get("isPaused", False)), + "is_changing": None, + }, + "active_scene": { + "path": None, + "guid": None, + "name": state.get("activeSceneName", "") or "", + }, + "selection": { + "count": int(state.get("selectionCount", 0) or 0), + "active_object_name": state.get("activeObjectName", None), + }, + }, + "activity": { + "phase": "unknown", + "since_unix_ms": now_ms, + "reasons": ["legacy_fallback"], + }, + "compilation": { + "is_compiling": bool(state.get("isCompiling", False)), + "is_domain_reload_pending": None, + "last_compile_started_unix_ms": None, + "last_compile_finished_unix_ms": None, + }, + "assets": { + "is_updating": bool(state.get("isUpdating", False)), + "external_changes_dirty": False, + "external_changes_last_seen_unix_ms": None, + "refresh": { + "is_refresh_in_progress": False, + "last_refresh_requested_unix_ms": None, + "last_refresh_finished_unix_ms": None, + }, + }, + "tests": { + "is_running": False, + "mode": None, + "started_unix_ms": None, + "started_by": "unknown", + "last_run": None, + }, + "transport": { + "unity_bridge_connected": None, + "last_message_unix_ms": None, + }, + } + + +def _enrich_advice_and_staleness(state_v2: dict[str, Any]) -> dict[str, Any]: + now_ms = _now_unix_ms() + observed = state_v2.get("observed_at_unix_ms") + try: + observed_ms = int(observed) + except Exception: + observed_ms = now_ms + + age_ms = max(0, now_ms - observed_ms) + # Conservative default: treat >2s as stale (covers common unfocused-editor throttling). + is_stale = age_ms > 2000 + + compilation = state_v2.get("compilation") or {} + tests = state_v2.get("tests") or {} + assets = state_v2.get("assets") or {} + refresh = (assets.get("refresh") or {}) if isinstance(assets, dict) else {} + + blocking: list[str] = [] + if compilation.get("is_compiling") is True: + blocking.append("compiling") + if compilation.get("is_domain_reload_pending") is True: + blocking.append("domain_reload") + if tests.get("is_running") is True: + blocking.append("running_tests") + if refresh.get("is_refresh_in_progress") is True: + blocking.append("asset_refresh") + if is_stale: + blocking.append("stale_status") + + ready_for_tools = len(blocking) == 0 + + state_v2["advice"] = { + "ready_for_tools": ready_for_tools, + "blocking_reasons": blocking, + "recommended_retry_after_ms": 0 if ready_for_tools else 500, + "recommended_next_action": "none" if ready_for_tools else "retry_later", + } + state_v2["staleness"] = {"age_ms": age_ms, "is_stale": is_stale} + return state_v2 + + +@mcp_for_unity_resource( + uri="unity://editor_state", + name="editor_state_v2", + description="Canonical editor readiness snapshot (v2). Includes advice and server-computed staleness.", +) +async def get_editor_state_v2(ctx: Context) -> MCPResponse: + unity_instance = get_unity_instance_from_context(ctx) + + # Try v2 snapshot first (Unity-side cache will make this fast once implemented). + response = await unity_transport.send_with_unity_instance( + async_send_command_with_retry, + unity_instance, + "get_editor_state_v2", + {}, + ) + + # If Unity returns a structured retry hint or error, surface it directly. + if isinstance(response, dict) and not response.get("success", True): + return MCPResponse(**response) + + # If v2 is unavailable (older plugin), fall back to legacy get_editor_state and map. + if not (isinstance(response, dict) and isinstance(response.get("data"), dict) and response["data"].get("schema_version")): + legacy = await unity_transport.send_with_unity_instance( + async_send_command_with_retry, + unity_instance, + "get_editor_state", + {}, + ) + if isinstance(legacy, dict) and not legacy.get("success", True): + return MCPResponse(**legacy) + state_v2 = _build_v2_from_legacy(legacy if isinstance(legacy, dict) else {}) + else: + state_v2 = response.get("data") if isinstance(response.get("data"), dict) else {} + # Ensure required v2 marker exists even if Unity returns partial. + state_v2.setdefault("schema_version", "unity-mcp/editor_state@2") + state_v2.setdefault("observed_at_unix_ms", _now_unix_ms()) + state_v2.setdefault("sequence", 0) + + # Ensure the returned snapshot is clearly associated with the targeted instance. + # (This matters when multiple Unity instances are connected and the client is polling readiness.) + unity_section = state_v2.get("unity") + if not isinstance(unity_section, dict): + unity_section = {} + state_v2["unity"] = unity_section + current_instance_id = unity_section.get("instance_id") + if current_instance_id in (None, ""): + if unity_instance: + unity_section["instance_id"] = unity_instance + else: + inferred = await _infer_single_instance_id(ctx) + if inferred: + unity_section["instance_id"] = inferred + + # External change detection (server-side): compute per instance based on project root path. + # This helps detect stale assets when external tools edit the filesystem. + try: + instance_id = unity_section.get("instance_id") + if isinstance(instance_id, str) and instance_id.strip(): + from services.resources.project_info import get_project_info + + # Cache the project root for this instance (best-effort). + proj_resp = await get_project_info(ctx) + proj = proj_resp.model_dump() if hasattr(proj_resp, "model_dump") else proj_resp + proj_data = proj.get("data") if isinstance(proj, dict) else None + project_root = proj_data.get("projectRoot") if isinstance(proj_data, dict) else None + if isinstance(project_root, str) and project_root.strip(): + external_changes_scanner.set_project_root(instance_id, project_root) + + ext = external_changes_scanner.update_and_get(instance_id) + + assets = state_v2.get("assets") + if not isinstance(assets, dict): + assets = {} + state_v2["assets"] = assets + # IMPORTANT: Unity's cached snapshot may include placeholder defaults; the server scanner is authoritative + # for external changes (filesystem edits outside Unity). Always overwrite these fields from the scanner. + assets["external_changes_dirty"] = bool(ext.get("external_changes_dirty", False)) + assets["external_changes_last_seen_unix_ms"] = ext.get("external_changes_last_seen_unix_ms") + # Extra bookkeeping fields (server-only) are safe to add under assets. + assets["external_changes_dirty_since_unix_ms"] = ext.get("dirty_since_unix_ms") + assets["external_changes_last_cleared_unix_ms"] = ext.get("last_cleared_unix_ms") + except Exception: + # Best-effort; do not fail readiness resource if filesystem scan can't run. + pass + + state_v2 = _enrich_advice_and_staleness(state_v2) + return MCPResponse(success=True, message="Retrieved editor state (v2).", data=state_v2) + + diff --git a/Server/src/services/state/external_changes_scanner.py b/Server/src/services/state/external_changes_scanner.py new file mode 100644 index 0000000..9227f7a --- /dev/null +++ b/Server/src/services/state/external_changes_scanner.py @@ -0,0 +1,246 @@ +from __future__ import annotations + +import os +import json +import time +from dataclasses import dataclass +from pathlib import Path +from typing import Iterable + + +def _now_unix_ms() -> int: + return int(time.time() * 1000) + + +def _in_pytest() -> bool: + # Keep scanner inert during the Python integration suite unless explicitly invoked. + return bool(os.environ.get("PYTEST_CURRENT_TEST")) + + +@dataclass +class ExternalChangesState: + project_root: str | None = None + last_scan_unix_ms: int | None = None + last_seen_mtime_ns: int | None = None + dirty: bool = False + dirty_since_unix_ms: int | None = None + external_changes_last_seen_unix_ms: int | None = None + last_cleared_unix_ms: int | None = None + # Cached package roots referenced by Packages/manifest.json "file:" dependencies + extra_roots: list[str] | None = None + manifest_last_mtime_ns: int | None = None + + +class ExternalChangesScanner: + """ + Lightweight external-changes detector using recursive max-mtime scan. + + This is intentionally conservative: + - It only marks dirty when it sees a strictly newer mtime than the baseline. + - It scans at most once per scan_interval_ms per instance to keep overhead bounded. + """ + + def __init__(self, *, scan_interval_ms: int = 1500, max_entries: int = 20000): + self._states: dict[str, ExternalChangesState] = {} + self._scan_interval_ms = int(scan_interval_ms) + self._max_entries = int(max_entries) + + def _get_state(self, instance_id: str) -> ExternalChangesState: + return self._states.setdefault(instance_id, ExternalChangesState()) + + def set_project_root(self, instance_id: str, project_root: str | None) -> None: + st = self._get_state(instance_id) + if project_root: + st.project_root = project_root + + def clear_dirty(self, instance_id: str) -> None: + st = self._get_state(instance_id) + st.dirty = False + st.dirty_since_unix_ms = None + st.last_cleared_unix_ms = _now_unix_ms() + # Reset baseline to “now” on next scan. + st.last_seen_mtime_ns = None + + def _scan_paths_max_mtime_ns(self, roots: Iterable[Path]) -> int | None: + newest: int | None = None + entries = 0 + + for root in roots: + if not root.exists(): + continue + + # Walk the tree; skip common massive/irrelevant dirs (Library/Temp/Logs). + for dirpath, dirnames, filenames in os.walk(str(root)): + entries += 1 + if entries > self._max_entries: + return newest + + dp = Path(dirpath) + name = dp.name.lower() + if name in {"library", "temp", "logs", "obj", ".git", "node_modules"}: + dirnames[:] = [] + continue + + # Allow skipping hidden directories quickly + dirnames[:] = [d for d in dirnames if not d.startswith(".")] + + for fn in filenames: + if fn.startswith("."): + continue + entries += 1 + if entries > self._max_entries: + return newest + p = dp / fn + try: + stat = p.stat() + except OSError: + continue + m = getattr(stat, "st_mtime_ns", None) + if m is None: + # Fallback when st_mtime_ns is unavailable + m = int(stat.st_mtime * 1_000_000_000) + newest = m if newest is None else max(newest, int(m)) + + return newest + + def _resolve_manifest_extra_roots(self, project_root: Path, st: ExternalChangesState) -> list[Path]: + """ + Parse Packages/manifest.json for local file: dependencies and resolve them to absolute paths. + Returns a list of Paths that exist and are directories. + """ + manifest_path = project_root / "Packages" / "manifest.json" + try: + stat = manifest_path.stat() + except OSError: + st.extra_roots = [] + st.manifest_last_mtime_ns = None + return [] + + mtime_ns = getattr(stat, "st_mtime_ns", int(stat.st_mtime * 1_000_000_000)) + if st.extra_roots is not None and st.manifest_last_mtime_ns == mtime_ns: + return [Path(p) for p in st.extra_roots if p] + + try: + raw = manifest_path.read_text(encoding="utf-8") + doc = json.loads(raw) + except Exception: + st.extra_roots = [] + st.manifest_last_mtime_ns = mtime_ns + return [] + + deps = doc.get("dependencies") if isinstance(doc, dict) else None + if not isinstance(deps, dict): + st.extra_roots = [] + st.manifest_last_mtime_ns = mtime_ns + return [] + + roots: list[str] = [] + base_dir = manifest_path.parent + + for _, ver in deps.items(): + if not isinstance(ver, str): + continue + v = ver.strip() + if not v.startswith("file:"): + continue + suffix = v[len("file:") :].strip() + # Handle file:///abs/path or file:/abs/path + if suffix.startswith("///"): + candidate = Path("/" + suffix.lstrip("/")) + elif suffix.startswith("/"): + candidate = Path(suffix) + else: + candidate = (base_dir / suffix).resolve() + try: + if candidate.exists() and candidate.is_dir(): + roots.append(str(candidate)) + except OSError: + continue + + # De-dupe, preserve order + deduped: list[str] = [] + seen = set() + for r in roots: + if r not in seen: + seen.add(r) + deduped.append(r) + + st.extra_roots = deduped + st.manifest_last_mtime_ns = mtime_ns + return [Path(p) for p in deduped if p] + + def update_and_get(self, instance_id: str) -> dict[str, int | bool | None]: + """ + Returns a small dict suitable for embedding in editor_state_v2.assets: + - external_changes_dirty + - external_changes_last_seen_unix_ms + - dirty_since_unix_ms + - last_cleared_unix_ms + """ + st = self._get_state(instance_id) + + if _in_pytest(): + return { + "external_changes_dirty": st.dirty, + "external_changes_last_seen_unix_ms": st.external_changes_last_seen_unix_ms, + "dirty_since_unix_ms": st.dirty_since_unix_ms, + "last_cleared_unix_ms": st.last_cleared_unix_ms, + } + + now = _now_unix_ms() + if st.last_scan_unix_ms is not None and (now - st.last_scan_unix_ms) < self._scan_interval_ms: + return { + "external_changes_dirty": st.dirty, + "external_changes_last_seen_unix_ms": st.external_changes_last_seen_unix_ms, + "dirty_since_unix_ms": st.dirty_since_unix_ms, + "last_cleared_unix_ms": st.last_cleared_unix_ms, + } + + st.last_scan_unix_ms = now + + project_root = st.project_root + if not project_root: + return { + "external_changes_dirty": st.dirty, + "external_changes_last_seen_unix_ms": st.external_changes_last_seen_unix_ms, + "dirty_since_unix_ms": st.dirty_since_unix_ms, + "last_cleared_unix_ms": st.last_cleared_unix_ms, + } + + root = Path(project_root) + paths = [root / "Assets", root / "ProjectSettings", root / "Packages"] + # Include any local package roots referenced by file: deps in Packages/manifest.json + try: + paths.extend(self._resolve_manifest_extra_roots(root, st)) + except Exception: + pass + newest = self._scan_paths_max_mtime_ns(paths) + if newest is None: + return { + "external_changes_dirty": st.dirty, + "external_changes_last_seen_unix_ms": st.external_changes_last_seen_unix_ms, + "dirty_since_unix_ms": st.dirty_since_unix_ms, + "last_cleared_unix_ms": st.last_cleared_unix_ms, + } + + if st.last_seen_mtime_ns is None: + st.last_seen_mtime_ns = newest + elif newest > st.last_seen_mtime_ns: + st.last_seen_mtime_ns = newest + st.external_changes_last_seen_unix_ms = now + if not st.dirty: + st.dirty = True + st.dirty_since_unix_ms = now + + return { + "external_changes_dirty": st.dirty, + "external_changes_last_seen_unix_ms": st.external_changes_last_seen_unix_ms, + "dirty_since_unix_ms": st.dirty_since_unix_ms, + "last_cleared_unix_ms": st.last_cleared_unix_ms, + } + + +# Global singleton (simple, process-local) +external_changes_scanner = ExternalChangesScanner() + + diff --git a/Server/src/services/tools/manage_asset.py b/Server/src/services/tools/manage_asset.py index b317415..d1bc9dd 100644 --- a/Server/src/services/tools/manage_asset.py +++ b/Server/src/services/tools/manage_asset.py @@ -12,6 +12,7 @@ from services.tools import get_unity_instance_from_context from services.tools.utils import parse_json_payload, coerce_int from transport.unity_transport import send_with_unity_instance from transport.legacy.unity_connection import async_send_command_with_retry +from services.tools.preflight import preflight @mcp_for_unity_tool( @@ -47,6 +48,12 @@ async def manage_asset( ) -> dict[str, Any]: unity_instance = get_unity_instance_from_context(ctx) + # Best-effort guard: if Unity is compiling/reloading or known external changes are pending, + # wait/refresh to avoid stale reads and flaky timeouts. + gate = await preflight(ctx, wait_for_no_compile=True, refresh_if_dirty=True) + if gate is not None: + return gate.model_dump() + def _parse_properties_string(raw: str) -> tuple[dict[str, Any] | None, str | None]: try: parsed = json.loads(raw) diff --git a/Server/src/services/tools/manage_gameobject.py b/Server/src/services/tools/manage_gameobject.py index 4319066..c013dfb 100644 --- a/Server/src/services/tools/manage_gameobject.py +++ b/Server/src/services/tools/manage_gameobject.py @@ -8,6 +8,7 @@ from services.tools import get_unity_instance_from_context from transport.unity_transport import send_with_unity_instance from transport.legacy.unity_connection import async_send_command_with_retry from services.tools.utils import coerce_bool, parse_json_payload, coerce_int +from services.tools.preflight import preflight @mcp_for_unity_tool( @@ -92,6 +93,10 @@ async def manage_gameobject( # Removed session_state import unity_instance = get_unity_instance_from_context(ctx) + gate = await preflight(ctx, wait_for_no_compile=True, refresh_if_dirty=True) + if gate is not None: + return gate.model_dump() + if action is None: return { "success": False, diff --git a/Server/src/services/tools/manage_scene.py b/Server/src/services/tools/manage_scene.py index bd4502c..ed13478 100644 --- a/Server/src/services/tools/manage_scene.py +++ b/Server/src/services/tools/manage_scene.py @@ -6,6 +6,7 @@ from services.tools import get_unity_instance_from_context from services.tools.utils import coerce_int, coerce_bool from transport.unity_transport import send_with_unity_instance from transport.legacy.unity_connection import async_send_command_with_retry +from services.tools.preflight import preflight @mcp_for_unity_tool( @@ -40,6 +41,9 @@ async def manage_scene( # Get active instance from session state # Removed session_state import unity_instance = get_unity_instance_from_context(ctx) + gate = await preflight(ctx, wait_for_no_compile=True, refresh_if_dirty=True) + if gate is not None: + return gate.model_dump() try: coerced_build_index = coerce_int(build_index, default=None) coerced_super_size = coerce_int(screenshot_super_size, default=None) diff --git a/Server/src/services/tools/preflight.py b/Server/src/services/tools/preflight.py new file mode 100644 index 0000000..8b44365 --- /dev/null +++ b/Server/src/services/tools/preflight.py @@ -0,0 +1,107 @@ +from __future__ import annotations + +import asyncio +import os +import time +from typing import Any + +from models import MCPResponse + + +def _in_pytest() -> bool: + # Integration tests in this repo stub transports and do not run against a live Unity editor. + # Preflight must be a no-op in that environment to avoid breaking the existing test suite. + return bool(os.environ.get("PYTEST_CURRENT_TEST")) + + +def _busy(reason: str, retry_after_ms: int) -> MCPResponse: + return MCPResponse( + success=False, + error="busy", + message=reason, + hint="retry", + data={"reason": reason, "retry_after_ms": int(retry_after_ms)}, + ) + + +async def preflight( + ctx, + *, + requires_no_tests: bool = False, + wait_for_no_compile: bool = False, + refresh_if_dirty: bool = False, + max_wait_s: float = 30.0, +) -> MCPResponse | None: + """ + Server-side preflight guard used by tools so they behave safely even if the client never reads resources. + + Returns: + - MCPResponse busy/retry payload when the tool should not proceed right now + - None when the tool should proceed normally + """ + if _in_pytest(): + return None + + # Load canonical v2 state (server enriches advice + staleness). + try: + from services.resources.editor_state_v2 import get_editor_state_v2 + state_resp = await get_editor_state_v2(ctx) + state = state_resp.model_dump() if hasattr(state_resp, "model_dump") else state_resp + except Exception: + # If we cannot determine readiness, fall back to proceeding (tools already contain retry logic). + return None + + if not isinstance(state, dict) or not state.get("success", False): + # Unknown state; proceed rather than blocking (avoids false positives when Unity is reachable but status isn't). + return None + + data = state.get("data") + if not isinstance(data, dict): + return None + + # Optional refresh-if-dirty + if refresh_if_dirty: + assets = data.get("assets") + if isinstance(assets, dict) and assets.get("external_changes_dirty") is True: + try: + from services.tools.refresh_unity import refresh_unity + await refresh_unity(ctx, mode="if_dirty", scope="all", compile="request", wait_for_ready=True) + except Exception: + # Best-effort only; fall through to normal tool dispatch. + pass + + # Tests running: fail fast for tools that require exclusivity. + if requires_no_tests: + tests = data.get("tests") + if isinstance(tests, dict) and tests.get("is_running") is True: + return _busy("tests_running", 5000) + + # Compilation: optionally wait for a bounded time. + if wait_for_no_compile: + deadline = time.monotonic() + float(max_wait_s) + while True: + compilation = data.get("compilation") if isinstance(data, dict) else None + is_compiling = isinstance(compilation, dict) and compilation.get("is_compiling") is True + is_domain_reload_pending = isinstance(compilation, dict) and compilation.get("is_domain_reload_pending") is True + if not is_compiling and not is_domain_reload_pending: + break + if time.monotonic() >= deadline: + return _busy("compiling", 500) + await asyncio.sleep(0.25) + + # Refresh state for the next loop iteration. + try: + from services.resources.editor_state_v2 import get_editor_state_v2 + state_resp = await get_editor_state_v2(ctx) + state = state_resp.model_dump() if hasattr(state_resp, "model_dump") else state_resp + data = state.get("data") if isinstance(state, dict) else None + if not isinstance(data, dict): + return None + except Exception: + return None + + # Staleness: if the snapshot is stale, proceed (tools will still run), but callers that read resources can back off. + # In future we may make this strict for some tools. + return None + + diff --git a/Server/src/services/tools/refresh_unity.py b/Server/src/services/tools/refresh_unity.py new file mode 100644 index 0000000..47dc41e --- /dev/null +++ b/Server/src/services/tools/refresh_unity.py @@ -0,0 +1,90 @@ +from __future__ import annotations + +import asyncio +import time +from typing import Annotated, Any, Literal + +from fastmcp import Context + +from models import MCPResponse +from services.registry import mcp_for_unity_tool +from services.tools import get_unity_instance_from_context +import transport.unity_transport as unity_transport +from transport.legacy.unity_connection import async_send_command_with_retry +from services.state.external_changes_scanner import external_changes_scanner + + +@mcp_for_unity_tool( + description="Request a Unity asset database refresh and optionally a script compilation. Can optionally wait for readiness." +) +async def refresh_unity( + ctx: Context, + mode: Annotated[Literal["if_dirty", "force"], "Refresh mode"] = "if_dirty", + scope: Annotated[Literal["assets", "scripts", "all"], "Refresh scope"] = "all", + compile: Annotated[Literal["none", "request"], "Whether to request compilation"] = "none", + wait_for_ready: Annotated[bool, "If true, wait until editor_state.advice.ready_for_tools is true"] = True, +) -> MCPResponse | dict[str, Any]: + unity_instance = get_unity_instance_from_context(ctx) + + params: dict[str, Any] = { + "mode": mode, + "scope": scope, + "compile": compile, + "wait_for_ready": bool(wait_for_ready), + } + + recovered_from_disconnect = False + response = await unity_transport.send_with_unity_instance( + async_send_command_with_retry, + unity_instance, + "refresh_unity", + params, + ) + + # Option A: treat disconnects / retry hints as recoverable when wait_for_ready is true. + # Unity can legitimately disconnect during refresh/compile/domain reload, so callers should not + # interpret that as a hard failure (#503-style loops). + if isinstance(response, dict) and not response.get("success", True): + hint = response.get("hint") + err = (response.get("error") or response.get("message") or "") + is_retryable = (hint == "retry") or ("disconnected" in str(err).lower()) + if (not wait_for_ready) or (not is_retryable): + return MCPResponse(**response) + recovered_from_disconnect = True + + # Optional server-side wait loop (defensive): if Unity tool doesn't wait or returns quickly, + # poll the canonical editor_state v2 resource until ready or timeout. + if wait_for_ready: + timeout_s = 60.0 + start = time.monotonic() + from services.resources.editor_state_v2 import get_editor_state_v2 + + while time.monotonic() - start < timeout_s: + state_resp = await get_editor_state_v2(ctx) + state = state_resp.model_dump() if hasattr(state_resp, "model_dump") else state_resp + data = (state or {}).get("data") if isinstance(state, dict) else None + advice = (data or {}).get("advice") if isinstance(data, dict) else None + if isinstance(advice, dict) and advice.get("ready_for_tools") is True: + break + await asyncio.sleep(0.25) + + # After readiness is restored, clear any external-dirty flag for this instance so future tools can proceed cleanly. + try: + from services.resources.editor_state_v2 import _infer_single_instance_id + + inst = unity_instance or await _infer_single_instance_id(ctx) + if inst: + external_changes_scanner.clear_dirty(inst) + except Exception: + pass + + if recovered_from_disconnect: + return MCPResponse( + success=True, + message="Refresh recovered after Unity disconnect/retry; editor is ready.", + data={"recovered_from_disconnect": True}, + ) + + return MCPResponse(**response) if isinstance(response, dict) else response + + diff --git a/Server/src/services/tools/run_tests.py b/Server/src/services/tools/run_tests.py index 4aecc4f..870bc25 100644 --- a/Server/src/services/tools/run_tests.py +++ b/Server/src/services/tools/run_tests.py @@ -10,6 +10,7 @@ from services.tools import get_unity_instance_from_context from services.tools.utils import coerce_int from transport.unity_transport import send_with_unity_instance from transport.legacy.unity_connection import async_send_command_with_retry +from services.tools.preflight import preflight class RunTestsSummary(BaseModel): @@ -42,7 +43,7 @@ class RunTestsResponse(MCPResponse): @mcp_for_unity_tool( - description="Runs Unity tests for the specified mode" + description="Runs Unity tests synchronously (blocks until complete). Prefer run_tests_async for non-blocking execution with progress polling." ) async def run_tests( ctx: Context, @@ -54,9 +55,13 @@ async def run_tests( assembly_names: Annotated[list[str] | str, "Assembly names to filter tests by"] | None = None, include_failed_tests: Annotated[bool, "Include details for failed/skipped tests only (default: false)"] = False, include_details: Annotated[bool, "Include details for all tests (default: false)"] = False, -) -> RunTestsResponse: +) -> RunTestsResponse | MCPResponse: unity_instance = get_unity_instance_from_context(ctx) + gate = await preflight(ctx, requires_no_tests=True, wait_for_no_compile=True, refresh_if_dirty=True) + if isinstance(gate, MCPResponse): + return gate + # Coerce string or list to list of strings def _coerce_string_list(value) -> list[str] | None: if value is None: @@ -97,5 +102,19 @@ async def run_tests( params["includeDetails"] = True response = await send_with_unity_instance(async_send_command_with_retry, unity_instance, "run_tests", params) - await ctx.info(f'Response {response}') + + # If Unity indicates a run is already active, return a structured "busy" response rather than + # letting clients interpret this as a generic failure (avoids #503 retry loops). + if isinstance(response, dict) and not response.get("success", True): + err = (response.get("error") or response.get("message") or "").strip() + if "test run is already in progress" in err.lower(): + return MCPResponse( + success=False, + error="tests_running", + message=err, + hint="retry", + data={"reason": "tests_running", "retry_after_ms": 5000}, + ) + return MCPResponse(**response) + return RunTestsResponse(**response) if isinstance(response, dict) else response diff --git a/Server/src/services/tools/test_jobs.py b/Server/src/services/tools/test_jobs.py new file mode 100644 index 0000000..66ef619 --- /dev/null +++ b/Server/src/services/tools/test_jobs.py @@ -0,0 +1,94 @@ +"""Async Unity Test Runner jobs: start + poll.""" +from __future__ import annotations + +from typing import Annotated, Any, Literal + +from fastmcp import Context + +from models import MCPResponse +from services.registry import mcp_for_unity_tool +from services.tools import get_unity_instance_from_context +from services.tools.preflight import preflight +import transport.unity_transport as unity_transport +from transport.legacy.unity_connection import async_send_command_with_retry + + +@mcp_for_unity_tool(description="Starts a Unity test run asynchronously and returns a job_id immediately. Preferred over run_tests for long-running suites. Poll with get_test_job for progress.") +async def run_tests_async( + ctx: Context, + mode: Annotated[Literal["EditMode", "PlayMode"], "Unity test mode to run"] = "EditMode", + test_names: Annotated[list[str] | str, "Full names of specific tests to run"] | None = None, + group_names: Annotated[list[str] | str, "Same as test_names, except it allows for Regex"] | None = None, + category_names: Annotated[list[str] | str, "NUnit category names to filter by"] | None = None, + assembly_names: Annotated[list[str] | str, "Assembly names to filter tests by"] | None = None, + include_failed_tests: Annotated[bool, "Include details for failed/skipped tests only (default: false)"] = False, + include_details: Annotated[bool, "Include details for all tests (default: false)"] = False, +) -> dict[str, Any] | MCPResponse: + unity_instance = get_unity_instance_from_context(ctx) + + gate = await preflight(ctx, requires_no_tests=True, wait_for_no_compile=True, refresh_if_dirty=True) + if isinstance(gate, MCPResponse): + return gate + + def _coerce_string_list(value) -> list[str] | None: + if value is None: + return None + if isinstance(value, str): + return [value] if value.strip() else None + if isinstance(value, list): + result = [str(v).strip() for v in value if v and str(v).strip()] + return result if result else None + return None + + params: dict[str, Any] = {"mode": mode} + if (t := _coerce_string_list(test_names)): + params["testNames"] = t + if (g := _coerce_string_list(group_names)): + params["groupNames"] = g + if (c := _coerce_string_list(category_names)): + params["categoryNames"] = c + if (a := _coerce_string_list(assembly_names)): + params["assemblyNames"] = a + if include_failed_tests: + params["includeFailedTests"] = True + if include_details: + params["includeDetails"] = True + + response = await unity_transport.send_with_unity_instance( + async_send_command_with_retry, + unity_instance, + "run_tests_async", + params, + ) + + if isinstance(response, dict) and not response.get("success", True): + return MCPResponse(**response) + return response if isinstance(response, dict) else MCPResponse(success=False, error=str(response)).model_dump() + + +@mcp_for_unity_tool(description="Polls an async Unity test job by job_id.") +async def get_test_job( + ctx: Context, + job_id: Annotated[str, "Job id returned by run_tests_async"], + include_failed_tests: Annotated[bool, "Include details for failed/skipped tests only (default: false)"] = False, + include_details: Annotated[bool, "Include details for all tests (default: false)"] = False, +) -> dict[str, Any] | MCPResponse: + unity_instance = get_unity_instance_from_context(ctx) + + params: dict[str, Any] = {"job_id": job_id} + if include_failed_tests: + params["includeFailedTests"] = True + if include_details: + params["includeDetails"] = True + + response = await unity_transport.send_with_unity_instance( + async_send_command_with_retry, + unity_instance, + "get_test_job", + params, + ) + if isinstance(response, dict) and not response.get("success", True): + return MCPResponse(**response) + return response if isinstance(response, dict) else MCPResponse(success=False, error=str(response)).model_dump() + + diff --git a/Server/tests/integration/test_editor_state_v2_contract.py b/Server/tests/integration/test_editor_state_v2_contract.py new file mode 100644 index 0000000..4d58ee9 --- /dev/null +++ b/Server/tests/integration/test_editor_state_v2_contract.py @@ -0,0 +1,58 @@ +import pytest + +from services.registry import get_registered_resources + +from .test_helpers import DummyContext + + +@pytest.mark.asyncio +async def test_editor_state_v2_is_registered_and_has_contract_fields(monkeypatch): + """ + Red test: we expect a canonical v2 resource `unity://editor_state` with required top-level fields. + + Today, only `unity://editor/state` exists and is minimal. + """ + # Import the v2 module to ensure it registers its decorator without disturbing global registry state. + import services.resources.editor_state_v2 # noqa: F401 + + resources = get_registered_resources() + + v2 = next((r for r in resources if r.get("uri") == "unity://editor_state"), None) + assert v2 is not None, ( + "Expected canonical readiness resource `unity://editor_state` to be registered. " + "This is required so clients can poll readiness/staleness and avoid tool loops." + ) + + async def fake_send_with_unity_instance(send_fn, unity_instance, command_type, params, **kwargs): + # Minimal stub payload for v2 resource tests. The server layer should enrich with staleness/advice. + assert command_type in {"get_editor_state_v2", "get_editor_state"} + return { + "success": True, + "data": { + "schema_version": "unity-mcp/editor_state@2", + "observed_at_unix_ms": 1730000000000, + "sequence": 1, + "compilation": {"is_compiling": False, "is_domain_reload_pending": False}, + "tests": {"is_running": False}, + }, + } + + # Patch transport so the resource can be invoked without Unity running. + import transport.unity_transport as unity_transport + monkeypatch.setattr(unity_transport, "send_with_unity_instance", fake_send_with_unity_instance) + + result = await v2["func"](DummyContext()) + payload = result.model_dump() if hasattr(result, "model_dump") else result + assert isinstance(payload, dict) + + # Contract assertions (top-level) + assert payload.get("success") is True + data = payload.get("data") + assert isinstance(data, dict) + assert data.get("schema_version") == "unity-mcp/editor_state@2" + assert "observed_at_unix_ms" in data + assert "sequence" in data + assert "advice" in data + assert "staleness" in data + + diff --git a/Server/tests/integration/test_external_changes_scanner.py b/Server/tests/integration/test_external_changes_scanner.py new file mode 100644 index 0000000..00e8af2 --- /dev/null +++ b/Server/tests/integration/test_external_changes_scanner.py @@ -0,0 +1,86 @@ +import os +import time +from pathlib import Path + + +def test_external_changes_scanner_marks_dirty_and_clears(tmp_path, monkeypatch): + # Ensure the scanner is active for this unit-style test (not gated by PYTEST_CURRENT_TEST). + monkeypatch.delenv("PYTEST_CURRENT_TEST", raising=False) + + from services.state.external_changes_scanner import ExternalChangesScanner + + # Create a minimal Unity-like layout + root = tmp_path / "Project" + (root / "Assets").mkdir(parents=True) + (root / "ProjectSettings").mkdir(parents=True) + (root / "Packages").mkdir(parents=True) + + inst = "Test@deadbeef" + s = ExternalChangesScanner(scan_interval_ms=0, max_entries=10000) + s.set_project_root(inst, str(root)) + + # Create a file before baseline so the initial scan establishes a stable reference point. + p = root / "Assets" / "x.txt" + p.write_text("hi") + + # Baseline scan: should not be dirty. + first = s.update_and_get(inst) + assert first["external_changes_dirty"] is False + + # Touch the file and scan again: should become dirty. + now = time.time() + os.utime(p, (now + 10.0, now + 10.0)) + + second = s.update_and_get(inst) + assert second["external_changes_dirty"] is True + assert isinstance(second["external_changes_last_seen_unix_ms"], int) + assert isinstance(second["dirty_since_unix_ms"], int) + + # Clear and confirm dirty flag resets. + s.clear_dirty(inst) + third = s.update_and_get(inst) + assert third["external_changes_dirty"] is False + assert isinstance(third["last_cleared_unix_ms"], int) + + +def test_external_changes_scanner_includes_file_dependency_roots(tmp_path, monkeypatch): + # Ensure the scanner is active for this unit-style test (not gated by PYTEST_CURRENT_TEST). + monkeypatch.delenv("PYTEST_CURRENT_TEST", raising=False) + + from services.state.external_changes_scanner import ExternalChangesScanner + + # Unity project root + root = tmp_path / "Project" + (root / "Assets").mkdir(parents=True) + (root / "ProjectSettings").mkdir(parents=True) + (root / "Packages").mkdir(parents=True) + + # External local package root (outside project root) + pkg = tmp_path / "ExternalPkg" + (pkg / "Editor").mkdir(parents=True) + target = pkg / "Editor" / "Some.cs" + target.write_text("// v1") + + # manifest.json referencing file: dependency + manifest = root / "Packages" / "manifest.json" + manifest.write_text( + '{\n "dependencies": {\n "com.example.pkg": "file:../../ExternalPkg"\n }\n}\n', + encoding="utf-8", + ) + + inst = "Test@deadbeef" + s = ExternalChangesScanner(scan_interval_ms=0, max_entries=10000) + s.set_project_root(inst, str(root)) + + # Baseline scan captures current mtimes across project + external pkg + baseline = s.update_and_get(inst) + assert baseline["external_changes_dirty"] is False + + # Touch external package file and scan again -> should mark dirty + now = time.time() + os.utime(target, (now + 10.0, now + 10.0)) + + changed = s.update_and_get(inst) + assert changed["external_changes_dirty"] is True + + diff --git a/Server/tests/integration/test_refresh_unity_registration.py b/Server/tests/integration/test_refresh_unity_registration.py new file mode 100644 index 0000000..5529029 --- /dev/null +++ b/Server/tests/integration/test_refresh_unity_registration.py @@ -0,0 +1,14 @@ +from services.registry import get_registered_tools + + +def test_refresh_unity_tool_is_registered(): + """ + Red test: we expect an explicit refresh tool to exist so callers can force an import/refresh/compile cycle. + """ + # Import the specific module to ensure it registers its decorator without disturbing global registry state. + import services.tools.refresh_unity # noqa: F401 + + names = {t.get("name") for t in get_registered_tools()} + assert "refresh_unity" in names + + diff --git a/Server/tests/integration/test_refresh_unity_retry_recovery.py b/Server/tests/integration/test_refresh_unity_retry_recovery.py new file mode 100644 index 0000000..27a7022 --- /dev/null +++ b/Server/tests/integration/test_refresh_unity_retry_recovery.py @@ -0,0 +1,46 @@ +import pytest + +from models import MCPResponse +from services.state.external_changes_scanner import external_changes_scanner +from services.state.external_changes_scanner import ExternalChangesState + +from .test_helpers import DummyContext + + +@pytest.mark.asyncio +async def test_refresh_unity_recovers_from_retry_disconnect(monkeypatch): + """ + Option A: if Unity disconnects and the transport returns hint=retry, refresh_unity(wait_for_ready=true) + should poll readiness and then return success + clear external dirty. + """ + from services.tools.refresh_unity import refresh_unity + + ctx = DummyContext() + ctx.set_state("unity_instance", "UnityMCPTests@cc8756d4cce0805a") + + # Seed dirty state + inst = "UnityMCPTests@cc8756d4cce0805a" + external_changes_scanner._states[inst] = ExternalChangesState(dirty=True, dirty_since_unix_ms=1) + + async def fake_send_with_unity_instance(send_fn, unity_instance, command_type, params, **kwargs): + assert command_type == "refresh_unity" + return {"success": False, "error": "disconnected", "hint": "retry"} + + async def fake_get_editor_state_v2(_ctx): + return MCPResponse(success=True, data={"advice": {"ready_for_tools": True}}) + + import services.tools.refresh_unity as refresh_mod + monkeypatch.setattr(refresh_mod.unity_transport, "send_with_unity_instance", fake_send_with_unity_instance) + + import services.resources.editor_state_v2 as esv2_mod + monkeypatch.setattr(esv2_mod, "get_editor_state_v2", fake_get_editor_state_v2) + + resp = await refresh_unity(ctx, wait_for_ready=True) + payload = resp.model_dump() if hasattr(resp, "model_dump") else resp + assert payload["success"] is True + assert payload.get("data", {}).get("recovered_from_disconnect") is True + + # Dirty should be cleared + assert external_changes_scanner._states[inst].dirty is False + + diff --git a/Server/tests/integration/test_run_tests_busy_semantics.py b/Server/tests/integration/test_run_tests_busy_semantics.py new file mode 100644 index 0000000..ca72b39 --- /dev/null +++ b/Server/tests/integration/test_run_tests_busy_semantics.py @@ -0,0 +1,36 @@ +import pytest + +from .test_helpers import DummyContext + + +@pytest.mark.asyncio +async def test_run_tests_returns_busy_when_unity_reports_already_in_progress(monkeypatch): + """ + Red test (#503): if Unity reports a test run is already in progress, the tool should return a + structured Busy response quickly (retry hint + retry_after_ms) rather than looking like a generic failure. + """ + import services.tools.run_tests as run_tests_mod + + async def fake_send_with_unity_instance(send_fn, unity_instance, command_type, params, **kwargs): + assert command_type == "run_tests" + # This mirrors the Unity-side exception message thrown by TestRunnerService today. + return { + "success": False, + "error": "A Unity test run is already in progress.", + } + + monkeypatch.setattr(run_tests_mod, "send_with_unity_instance", fake_send_with_unity_instance) + + result = await run_tests_mod.run_tests(ctx=DummyContext(), mode="EditMode") + payload = result.model_dump() if hasattr(result, "model_dump") else result + + assert payload.get("success") is False + # Desired new behavior: provide an explicit retry hint + suggested backoff. + assert payload.get("hint") == "retry" + data = payload.get("data") or {} + assert isinstance(data, dict) + assert data.get("reason") in {"tests_running", "busy"} + assert isinstance(data.get("retry_after_ms"), int) + assert data.get("retry_after_ms") >= 500 + + diff --git a/Server/tests/integration/test_test_jobs_async.py b/Server/tests/integration/test_test_jobs_async.py new file mode 100644 index 0000000..a27a572 --- /dev/null +++ b/Server/tests/integration/test_test_jobs_async.py @@ -0,0 +1,52 @@ +import pytest + +from .test_helpers import DummyContext + + +@pytest.mark.asyncio +async def test_run_tests_async_forwards_params(monkeypatch): + from services.tools.test_jobs import run_tests_async + + captured = {} + + async def fake_send_with_unity_instance(send_fn, unity_instance, command_type, params, **kwargs): + captured["command_type"] = command_type + captured["params"] = params + return {"success": True, "data": {"job_id": "abc123", "status": "running"}} + + import services.tools.test_jobs as mod + monkeypatch.setattr(mod.unity_transport, "send_with_unity_instance", fake_send_with_unity_instance) + + resp = await run_tests_async( + DummyContext(), + mode="EditMode", + test_names="MyNamespace.MyTests.TestA", + include_details=True, + ) + assert captured["command_type"] == "run_tests_async" + assert captured["params"]["mode"] == "EditMode" + assert captured["params"]["testNames"] == ["MyNamespace.MyTests.TestA"] + assert captured["params"]["includeDetails"] is True + assert resp["success"] is True + + +@pytest.mark.asyncio +async def test_get_test_job_forwards_job_id(monkeypatch): + from services.tools.test_jobs import get_test_job + + captured = {} + + async def fake_send_with_unity_instance(send_fn, unity_instance, command_type, params, **kwargs): + captured["command_type"] = command_type + captured["params"] = params + return {"success": True, "data": {"job_id": params["job_id"], "status": "running"}} + + import services.tools.test_jobs as mod + monkeypatch.setattr(mod.unity_transport, "send_with_unity_instance", fake_send_with_unity_instance) + + resp = await get_test_job(DummyContext(), job_id="job-1") + assert captured["command_type"] == "get_test_job" + assert captured["params"]["job_id"] == "job-1" + assert resp["data"]["job_id"] == "job-1" + + diff --git a/Server/uv.lock b/Server/uv.lock index 1ed5979..cae446f 100644 --- a/Server/uv.lock +++ b/Server/uv.lock @@ -809,7 +809,7 @@ wheels = [ [[package]] name = "mcpforunityserver" -version = "8.3.0" +version = "8.6.0" source = { editable = "." } dependencies = [ { name = "fastapi" }, diff --git a/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/DomainReloadResilienceTests.cs b/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/DomainReloadResilienceTests.cs index e7077f0..f2fe4ca 100644 --- a/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/DomainReloadResilienceTests.cs +++ b/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/DomainReloadResilienceTests.cs @@ -11,9 +11,15 @@ namespace MCPForUnityTests.Editor.Tools { /// /// Tests for domain reload resilience - ensuring MCP requests succeed even during Unity domain reloads. + /// + /// These tests are marked [Explicit] because they trigger script compilation which can stall + /// subsequent tests' internal coroutine waits when Unity is backgrounded. The MCP workflow + /// itself is unaffected - socket messages provide external stimulus that keeps Unity responsive. + /// + /// Run these explicitly when needed, ideally with Unity foregrounded or first in the run. /// [Category("domain_reload")] - [Explicit("Intentionally triggers script compilation/domain reload; run explicitly to avoid slowing/flaking cold-start EditMode runs.")] + [Explicit("Triggers compilation that can stall subsequent tests. MCP workflow unaffected - see class docs.")] public class DomainReloadResilienceTests { private const string TempDir = "Assets/Temp/DomainReloadTests"; @@ -25,14 +31,14 @@ namespace MCPForUnityTests.Editor.Tools if (!AssetDatabase.IsValidFolder(TempDir)) { Directory.CreateDirectory(TempDir); - AssetDatabase.Refresh(); + AssetDatabase.Refresh(ImportAssetOptions.ForceSynchronousImport); } } [TearDown] public void TearDown() { - // Clean up temp directory + // Clean up temp directory - this deletes any scripts we created if (AssetDatabase.IsValidFolder(TempDir)) { AssetDatabase.DeleteAsset(TempDir); @@ -48,6 +54,10 @@ namespace MCPForUnityTests.Editor.Tools AssetDatabase.DeleteAsset("Assets/Temp"); } } + + // CRITICAL: Force a synchronous refresh and wait for any pending compilation to finish. + // This prevents leaving compilation running that could stall subsequent tests. + AssetDatabase.Refresh(ImportAssetOptions.ForceSynchronousImport); } /// @@ -72,7 +82,7 @@ public class StressTestScript : MonoBehaviour // Write script file File.WriteAllText(scriptPath, scriptContent); - AssetDatabase.Refresh(); + AssetDatabase.Refresh(ImportAssetOptions.ForceSynchronousImport); Debug.Log("[DomainReloadTest] Script created, domain reload triggered"); @@ -163,7 +173,7 @@ public class TestScript1 : MonoBehaviour }"; File.WriteAllText(scriptPath, scriptContent); - AssetDatabase.Refresh(); + AssetDatabase.Refresh(ImportAssetOptions.ForceSynchronousImport); Debug.Log("[DomainReloadTest] Script created"); @@ -211,7 +221,7 @@ public class RapidScript{i} : MonoBehaviour }}"; File.WriteAllText(scriptPath, scriptContent); - AssetDatabase.Refresh(); + AssetDatabase.Refresh(ImportAssetOptions.ForceSynchronousImport); Debug.Log($"[DomainReloadTest] Created script {i+1}/{scriptCount}"); diff --git a/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/ManageScriptableObjectTests.cs b/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/ManageScriptableObjectTests.cs index 2b2dd8a..32259d8 100644 --- a/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/ManageScriptableObjectTests.cs +++ b/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/ManageScriptableObjectTests.cs @@ -14,9 +14,10 @@ namespace MCPForUnityTests.Editor.Tools public class ManageScriptableObjectTests { private const string TempRoot = "Assets/Temp/ManageScriptableObjectTests"; - private const string NestedFolder = TempRoot + "/Nested/Deeper"; private const double UnityReadyTimeoutSeconds = 180.0; + private string _runRoot; + private string _nestedFolder; private string _createdAssetPath; private string _createdGuid; private string _matAPath; @@ -27,12 +28,12 @@ namespace MCPForUnityTests.Editor.Tools { yield return WaitForUnityReady(UnityReadyTimeoutSeconds); EnsureFolder("Assets/Temp"); - // Start from a clean slate every time (prevents intermittent setup failures). - if (AssetDatabase.IsValidFolder(TempRoot)) - { - AssetDatabase.DeleteAsset(TempRoot); - } + // Avoid deleting/recreating the entire TempRoot each test (can trigger heavy reimport churn). + // Instead, isolate each test in its own unique subfolder under TempRoot. EnsureFolder(TempRoot); + _runRoot = $"{TempRoot}/Run_{Guid.NewGuid():N}"; + EnsureFolder(_runRoot); + _nestedFolder = _runRoot + "/Nested/Deeper"; _createdAssetPath = null; _createdGuid = null; @@ -69,9 +70,9 @@ namespace MCPForUnityTests.Editor.Tools AssetDatabase.DeleteAsset(_matBPath); } - if (AssetDatabase.IsValidFolder(TempRoot)) + if (!string.IsNullOrEmpty(_runRoot) && AssetDatabase.IsValidFolder(_runRoot)) { - AssetDatabase.DeleteAsset(TempRoot); + AssetDatabase.DeleteAsset(_runRoot); } // Clean up parent Temp folder if empty @@ -89,21 +90,15 @@ namespace MCPForUnityTests.Editor.Tools } [Test] - public void Create_CreatesNestedFolders_PlacesAssetCorrectly_AndAppliesPatches() + public void Create_CreatesNestedFolders_PlacesAssetCorrectly() { var create = new JObject { ["action"] = "create", ["typeName"] = typeof(ManageScriptableObjectTestDefinition).FullName, - ["folderPath"] = NestedFolder, - ["assetName"] = "My_Test_Def", + ["folderPath"] = _nestedFolder, + ["assetName"] = "My_Test_Def_Placement", ["overwrite"] = true, - ["patches"] = new JArray - { - new JObject { ["propertyPath"] = "displayName", ["op"] = "set", ["value"] = "Hello" }, - new JObject { ["propertyPath"] = "baseNumber", ["op"] = "set", ["value"] = 42 }, - new JObject { ["propertyPath"] = "nested.note", ["op"] = "set", ["value"] = "note!" } - } }; var raw = ManageScriptableObject.HandleCommand(create); @@ -116,11 +111,47 @@ namespace MCPForUnityTests.Editor.Tools _createdGuid = data!["guid"]?.ToString(); _createdAssetPath = data["path"]?.ToString(); - Assert.IsTrue(AssetDatabase.IsValidFolder(NestedFolder), "Nested folder should be created."); - Assert.IsTrue(_createdAssetPath!.StartsWith(NestedFolder, StringComparison.Ordinal), $"Asset should be created under {NestedFolder}: {_createdAssetPath}"); + Assert.IsTrue(AssetDatabase.IsValidFolder(_nestedFolder), "Nested folder should be created."); + Assert.IsTrue(_createdAssetPath!.StartsWith(_nestedFolder, StringComparison.Ordinal), $"Asset should be created under {_nestedFolder}: {_createdAssetPath}"); Assert.IsTrue(_createdAssetPath.EndsWith(".asset", StringComparison.OrdinalIgnoreCase), "Asset should have .asset extension."); Assert.IsFalse(string.IsNullOrWhiteSpace(_createdGuid), "Expected guid in response."); + var asset = AssetDatabase.LoadAssetAtPath(_createdAssetPath); + Assert.IsNotNull(asset, "Created asset should load as TestDefinition."); + } + + [Test] + public void Create_AppliesPatches_ToCreatedAsset() + { + var create = new JObject + { + ["action"] = "create", + ["typeName"] = typeof(ManageScriptableObjectTestDefinition).FullName, + // Patching correctness does not depend on nested folder creation; keep this lightweight. + ["folderPath"] = _runRoot, + ["assetName"] = "My_Test_Def_Patches", + ["overwrite"] = true, + ["patches"] = new JArray + { + new JObject { ["propertyPath"] = "displayName", ["op"] = "set", ["value"] = "Hello" }, + new JObject { ["propertyPath"] = "baseNumber", ["op"] = "set", ["value"] = 42 }, + new JObject { ["propertyPath"] = "nested.note", ["op"] = "set", ["value"] = "note!" } + } + }; + + var raw = ManageScriptableObject.HandleCommand(create); + var result = raw as JObject ?? JObject.FromObject(raw); + Assert.IsTrue(result.Value("success"), result.ToString()); + + var data = result["data"] as JObject; + Assert.IsNotNull(data, "Expected data payload"); + + _createdGuid = data!["guid"]?.ToString(); + _createdAssetPath = data["path"]?.ToString(); + + Assert.IsTrue(_createdAssetPath!.StartsWith(_runRoot, StringComparison.Ordinal), $"Asset should be created under {_runRoot}: {_createdAssetPath}"); + Assert.IsFalse(string.IsNullOrWhiteSpace(_createdGuid), "Expected guid in response."); + var asset = AssetDatabase.LoadAssetAtPath(_createdAssetPath); Assert.IsNotNull(asset, "Created asset should load as TestDefinition."); Assert.AreEqual("Hello", asset!.DisplayName, "Private [SerializeField] string should be set via SerializedProperty."); @@ -136,7 +167,7 @@ namespace MCPForUnityTests.Editor.Tools { ["action"] = "create", ["typeName"] = typeof(ManageScriptableObjectTestDefinition).FullName, - ["folderPath"] = TempRoot, + ["folderPath"] = _runRoot, ["assetName"] = "Modify_Target", ["overwrite"] = true }; diff --git a/docs/README-DEV.md b/docs/README-DEV.md index ff20879..6269874 100644 --- a/docs/README-DEV.md +++ b/docs/README-DEV.md @@ -335,4 +335,15 @@ We provide a CI job to run a Natural Language Editing suite against the Unity te ### Windows uv path issues -- On Windows, when testing GUI clients, prefer the WinGet Links `uv.exe`; if multiple `uv.exe` exist, use "Choose `uv` Install Location" to pin the Links shim. \ No newline at end of file +- On Windows, when testing GUI clients, prefer the WinGet Links `uv.exe`; if multiple `uv.exe` exist, use "Choose `uv` Install Location" to pin the Links shim. + +### Domain Reload Tests Stall When Unity is Backgrounded + +Tests that trigger script compilation mid-run (e.g., `DomainReloadResilienceTests`) may stall when Unity is not the active window. This is an OS-level limitation—macOS throttles background application main threads, preventing compilation from completing. + +**Workarounds:** +- Run domain reload tests with Unity foregrounded +- Run them first in the test suite (before backgrounding Unity) +- Use the `[Explicit]` attribute to exclude them from default runs + +**Note:** The MCP workflow itself is unaffected—socket messages provide external stimulus that keeps Unity responsive even when backgrounded. This limitation only affects Unity's internal test coroutine waits. \ No newline at end of file diff --git a/docs/images/unity-mcp-ui-v8.6.png b/docs/images/unity-mcp-ui-v8.6.png new file mode 100644 index 0000000000000000000000000000000000000000..ffc397e3de0c368483403377a5aa48d8e9f6fac7 GIT binary patch literal 148984 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

{&%2CAfD{A4mF}L zF@?klW4kNvTh`JLyh_v>0+vDDQKOa}aixN*dV>P|Ftt;XZLSWb)0o4$Wst*L?35v> zY^~B4TUm2tPS~xm&+^1~N&K%DJ{I~crsJ>va}x;ACsN~iIP_2KR6kQ-A2pkE$;7}A zr|lDi#g35s#M0nw`NivsPc8KcFXnD2$nS$v zE@U%YS{*P3IocF|Dvf#nQ9&=KIof}h0lasYILqSa=^6bIJR|{@Wxq@ud07F}`TL^$hPu`G9jVZ-;?x zEJ5Uv(^hQ>T0WTajJ52D9sw=9pn`M3FsKP#vGD^9r?O1GrYbh^jyJ2pB;i`f07Z($_>m~VTi ztBBy-Q!4s0$*OwqymQRTf>4+YfE)6tvfejlT<-i=Whf1WM|!Avww%vWjI{0aXuzlF z5}il@xBa}A*VBZsgb|;*=tCOGxb*f;pJMQbje|p)S!S`li)gM0HF}bNphl>!ct55c zH)lv=s1khVHRTtX5CmnQ&9Ng#ZTa^6j?* z4l4eJ${X&(oHG(bYnSfTH13kHg*bNShQmc*)J6o%s*=XLHqja47+>t5`O^_ubGcR$ z3w4tvOo2XMkh_sqhf5el(v$Ws-zT2amZkCEf-=xf#)ot&ZBkhU`V}Hn!wnLNh~|J! z0)2M+AF|l(e6a33@;P^xmH*oYpQO)Zw91-4^Hw(cxP<~rjO)g-rTNy!Y?hH?UqxS@ z(NDS)NoUPQt_G(%+$|TxdA1KZXuj>eW0O8^nFf~wafi}9p{eNfV&m7lzeDRo8fxt+ zUw$vZG;Am$Ebn~mQ_MO8>V5>LQLnDt3qzkyc}zGkUOw>AK?-|2(LV=IbK)2Chn2+K zb`2B}6!r||Xu?BA5lPe+Bv;_J1QY$=!rLk{v2?HAWqF@6L%S5}f_V4)7%e6R&_SOl z@O2!Ed#E*2`QYap-X?E9s?d*$+}!XYWN0~wmKPK(@2l8j!+$6nkp@)Nn#^-ja5oWB z9ptJK!sZwBvpnSwUhg-Eja2f|vpfd%t#=A*<;C~;*xs!0j*`yakssBu43WO?iq_#f z5Z^7cgxeQVUR|7}Q{^g|dJZL!b~djwm62q!xg6@8tekpfmtFtOH}9sg4?GjGn{W!= zOU;QXXCM?C9VM(%AkCoOZO)wK7W;uI-Fglp`=-i=^`fAG^kTW>vRh>sLXh-i=l%qb z`-n*qTg`8pt>ZK9rWQsE)#QD?{a!tt#X|_cS;B{+?z21}>r-}=TnT%af&guI z@CVQD465OFWXmXGkLEHJ&e&Wv%<^M=53ox4NNy8U=%-Qq9t`e!N07Q9sm6#ml%9WY z+9bN3@NRD_NtdMR_u}OZ?6U23aR-A{EO}GE=-p`=d*yIE9QnpA0Vt^74qIQhz;k4c zH9hbBN*PTfkLEKlu2LX;R!6A6J9XEA>RwwfVx*%6LR&UI+4*1&@=Ea6;`1x*Yt5>| z#3Q=AsB=Jy>0Yv~sJC-5OBv@yS{OilZ>Wyw3L`xHB_s&_P+|L>i%1}vw~<(nQ^9~J zd-JtN_3$<1f5L!2{Dbiae>v3fzM5~lr#KQ*<;*Q9=m)tyeSG1o7ClpIXw1Czw{3Hw zLPe=ypA-)C?509=BJ$bHdtd8qREk%h8I=tM0UV8r}cJ-djgi`F2~wDhMK? zgoLC>2}o>V(D{h7)Ybi-JKiQbSbgv+H{F@cjtTU-+7*M9+5M~_m219 zZw!VGbZ~R;E7zKHuDR5@q?U?I>2~ooTGs`N_PdYf_Qd!pGZu#*@lMW~V>2vU-p~ z+WQrQEh}e>@n@OS-)E@5vi-;Y2g5LVyR~kJ#mUdH{OJ4$HDE=~E0%i*oKvgqqv^3i)*42T6X@EEra_!k!F?Gu>1VeZB zM+Roe4}C9B*|s`<5V^|9r_SMTq67o?jJa2ZpD1JhaqLaAC*12Y5;PPFTI_G#k9c=0 zKZ*)g9dCXz5hEzS9;x1sR%P;E#{rGb2Q!JXAA8&ro3RBDKYe+L;`yjPeMV!B@!Oa{+Q91To*9V5 zBb}Er)8F<%dj&`*QAOy-H4!h=`_CvaT^OUO*suDv_NJKWTK9^Q)k8b{d z!KOMZPKLOr`mh987FM0kbF9sA)pxa$t6YMxXiOUEBVGQunf|pw``fS7({EF5Q@D*+ zPPGU1eb9Z|*De|8rqex>zTf^`1TDeyxRCb~rWi|&eu#}W$-6%Bit~vPvdvo26O&qM5}8Y34;G9Fs>nNV z=e#uoW%B6rkn}i}ToSvP7Rlh~CN;5KZoC(M2aDRiCUep0%26Ht|DTYlHy9yoqtk(k zF~>%cIE2~T47*y)&R@Y5dd=7P5j$kK1_FSoVs>-6lK&TEN~i(f2(s=IK*V(!{%#h0 zb*`quxx=O1)mYahj&{Pgr|YNwLR`=P?h^B?mDn<=w1%^L6g=SKSXEFrFN z#BpL)(lTh5%nAD-d}JR_Ygd=2VyGRRgV=HSaOma#Kl_>~o0QR5w+>~0uK57Hy| z^`x$SR4sb`uuY9${Lf8@f&$?UsHe4C3sK<{Pz?3{<}hOtZg#;z z&$#Z!yO~n|#-`?mRlbqF@$b`(D1+f>%Noa0(R21THQ~#i`_p4Cosh_@Pg(sk=xSc3 z(HDMG^{Xt8p|Tis!Qhd0Bta)kY}2~NIAL7RkEP;PX`3z^^y2%D-6QF)Dh3k3P$B9C z4vM1Wg`L+5C0>2LgmR*>ws6e4BQ^km+9{glQ5Sp?H?wJ^5YvKErfXT5o0u20T7G38 zdix^y?q$ngk3=U-pJ<0S$Z1Z<1kqpRGQ0T1a)}d{fWV$%7`hYCVfu!dugMu} ziZ_?VJtt`6GEPFZ`k(9_@m6p!>RdpWi}(X|kYT_aqlTw#v2XQNN2tvx#9YHMDEm^z z$qX<~6sIXBvF9#^*zMOM^UK~XOvK#WG)drYI=Z1~b2%%g z%#CK3mhCp~+F|Auf@?(IC?Wr%sP-klS%%V8Of)97SNf=&QqCy#sqTJGjw4;CuADOhVIhRgU=i2pmC#0HdrH^eyt^kS$B?Hr1Pj)w?^OE7o6|AXK7HwIs~^rjBz7@z3@5TB>|!7%xnV)l^BcKVhLz>(&nzvhbL z{<;D&VUGjF?`Z>!RR1b_`Qy5SH-ad?-PP&3Z!ie+A&8cN(5hbYzTLzpci(>##zyi< zGkH*PTCK`@nCa;{Q`G_^;|K@F@!hNOVa0Yh34SqMs;1g0j8HOY|35 z1fRPeL994Mv_FxM%z7sZe$lk?1@3#7vl5SA%$M_VALR@=5eDv8P0e)AZ1yDY_YGV- zG>_2RA5R-F3)$y0H!-bdy>zMp^Rw&{-rzDoLqF!S{fk5nnrxTb7IdA`uaxU|NA~#0 zUA9ix64|B03E~plA*DeO=f)BcbGW}uKFfiy_cIX<2}|2w+0(weuNTF{HtMSPGnhtR zCgM`!$rXch{V<1{>l`Z?4KMHzIj?wx#Sraw(EHH=4`M>JDcU%0RPWm#fF&Iig(lBd zYeDJ^@0P z|Cxb*?;ahy#|f_k%kSjIwij>6u)~(;7rU z>T7SInOE~k0;)lX%g(m`t~%>^;eH5%DSh9)k;6H$0==g^`;|xEgs!B}-QH8B@S0`5 z!msgHnjGyQOi8V>ZPvxzS&;&HthjXyw&;o1ub0_>0v3HS6CieX>)zC)_Ehn zL6KZ`{L@2`_0rov=wG}Y^dfK9gRapS%{(t_Kx5vj51khx3mI5^)8B)3sWdo;0)rE2 zRbck7;3xvimGzZB-_}kjJ$C@yOU?2@0?`Jt_AW99ew9ci(5)du5ttT|T}C8k$6xKR zxs@}*Qj|Ew7lA3b#?z?8rpxszr!&3}d%omkZWeLvY6`5N;Z4z#;SqGG@l-E=9U&Nv!d%6GH#E!iMth?EPWyJ1}1u#C#Sy_BOH3NtfEh-E+uzgG~GvsHa)^NBTRc zVIx}#s@-gz)V9)vPZ!sgvh%gxRsT1R&$#yEj@!HUhqK}Djbf~taGmEk;)h)Uu)XGX zGEF59DNWx58n6QACB=9?b6vn6O7g!>8N zvzmGDNe~d_%6*v;XLC|`4``KJ2>S7-M1Le#ASRsb{P|Bnm>(PCGAv~F;q{8Z&=NGD zKQRm)JLz?CYNo>ND!sx@!Fj89t@&l}}}zs}G0*C`L1 zCG7vHdgu`+wu?VqF{)h7tITum&(`XIV#YoR8u8t`@2i$KCH-ODo4Mc+`&qEM1b3 z_hwL>#5hW(FSz`=YiZ4Cu<~{|PWCTUCG+?%DKft3W0cYurI`5Q>JDabJ+FFiL^l#8 z|3(ha1HS=l9-7lkc~?`S_bao_G(jtrDB_HJ+!pz6;Aluch>q~$y044*W;i}o>y?x3BU-|sEX`L_tQal3JiW67nBZ}AIC<5%GGw?xpv=q9DwjYbhS|h zYwRniYTj|i^qAhUYz(d6P|n&Z!u$odBV@dNxR%r6JIR91yJiQ-I_EwG2^_TID1jO3 zJNMX8+~b!qWZYiy+Dx!KB}UWS12Wt`ojlK-Q=m()^JGnT`+=?1?Rrp8wZ`dS#y)rk zAN&WIShYNxvoy>t1LvhJfMr}O-&Zedik*OYdyC?{*o;0I!R|8rd3KI5l8{92J8aG@ zpM1$LS5Vp8DtOqhKoeb#zX}=t_|AmAhWkak-z+foqszj}qw7Nw=ay0Bju84RwlAyH zY=yoB3y2JZ2j?w{z3o2M+OJUA2e0W~HfY}^xWD2CWZw3eln#@HgZ=DGJ*O3Z>lX4w(qBe=mx)=@0I2?NYKiS>C zE6S7{$)v_*D*b#i#AG`Bv&%F?gze{pI7^(Kz*Ix=T|m+eE*|uT*Kpp_cZ%qRAjmm1 zv|PZhO-6bywCAy6)y#&e-(XQ&{;u^joI~0ctH`{|q(FShlnVhroAy0vTIRnK*^2I9EtQ59YoFC<%M``_B8Tw0h z1*T;qGH_UdZOrJ&Flpgjy=g)c#^~|xBK5jpl)Ny}3sH`nGl29i_=EJ;R^SEW>Lsmj z#|ueHTvG^g>YFq*t^}?j{NqM`#@_0kBGRt?d<5BUPR${4RgZeA;ocY>z!Jcf$GH+k z{O3D@fSV(&!rU=+_PaFe;vVo=S&US|SzlS6@?%@uDK;XGZ9D}8#d_ioA|p_((xa2z z7E$i*aAWQ-u>L*9zZhzU+bD(_Q+Q^US^kcbBI2v`_^UyVbmUW$xsD$N|3gtHn~aNP zOB?({m;h@D?f1>pUvAwo?=>jF*Tl%-2&`auwI99YHo^c{G34pO<4I7%VOwVV`Uy@3 z>5gsf;>)LZ+@1D{;>Q=`#w7sKS^qI3WAJ|FsmUpd@ve8Ryj82CEaeVl@??7(37CS$ z4ivSUWVJnuiAwn6968ROaN+7_g6vX8rOWSFDD9~7Sdjx%I6HOBf;!9>ulKsxIKT)M z->|C0Z>Y=Ssn0R*FJp7IwcqUuO&uamy$2!volF?6d8hHU81HcJq}^fB%KW}O(S&$- zA*&Ln@sisxq{lVSsj$GSfa3iajDNF%;0s6M&tDGH0rl+Zm;xrM$VeH%MUo!HV7vUq z3cF=5XW1^^`Iz4?4aVKGo#m8let#yu9lp9&)Fo6O{Gt4lir}sjXF^sGhHxxl|FG`r z>k=mvkwmqZ`sB4d#Tg*U+1FF6NFUo}Ziptisr|x~i-ioF=9Bi_;hFT%Jw<72N2I7Q zQ_8&D>^CKQcTym1!lL530+%^y0^5@TFo{#qYTmOf{N7tMV< z`%$K{V@I`mCsus6R}Uv)%Z@z)ANeSw>?y>@Y<4y|H2j5xh7(g!jESwD@z9&A=D*c3 z#hBFUv&|_;H8x?Xu+pVf{Ue5JR0(bG!kI`(p`UUMdnB%!#E*6TQ}sHuX3aX>e@{u! z(}Bf@)k4e6SxfrF@ItYj4)3jnlAp7_LbrENvVpGSqvRto`HKETFVeSmI%eLX>uOwI z4CRFH4z@G0nBbH*V0vJ*F`3@iO=t{nq7Uj4Md71~VK`12<@^B)KVH`b}{0tN}@)6$a-#gx~ZGd4)N1n<_%GHn^k_oXp zq#@I$)_|Z=Y>A^wz~;bQ)wLn$j5H~z^^!uq0UordeT}Sj-eG9hEyT+*POgZli18; z?gKAI5k@$%=29aTziBzCxqakn3cdB~5&RS;VMz#O_;Qx}7G2Su`8xhf^&)KH3AT%OEpRS1yRnn^2A{_mKk$LK+ zH67|uwt#cKc9$tZ+>31sCk#pdI`Tc zAgVNqpkv4-b#dBD)YU>~qguI(Bz-u1C8}vcqy(FU=!M+wL9b4TSo8Gx`ARHijFx!y z8AA%5l&I+akSnimR7+X*^56Kt$@t*Duc}DMgM>khLFXRl{Ij06YAy69A|lsA9$dTD zTe&mg%!Wlxcs9=;J-;zJy!}=*r7Bur`!MFcp__qW=X>tMl_HM$95Vv;tABoZPCU79 zDzGT5Ou0*CPq-*N`y09TXqCsyT2H9tEkrg+8T&=<6TMt<<8B_^9o_VNjonPf)O_TU z{GX2jPxZ8Cz_p7TP&2K{xTCLLZv1kk;!S!}8sD7q+`d$BMk5&}6K0Q&ukJ1QFS2rH zX-Jlj_v+fck_Zp13tu~2*YJ~b_)Tku=$~2j$OXEXPOJD^XV{qR%H*Eh?7K2#pm?W1q5rn>0!Uh_`&zIk$V_=S6Ecf zhJq|)iEK6y0`SaGvh6Xh3&V1iz}WIyubowBp?EF>8K}FiRkoTbZa3p|lX=JNjC?RP zX;5jRP*G0OR>n%NbM3-c^%s#oThnm6%=3kCJ}al4bZZiAn>R0IThNz7TOcIP!(AU= zQ5|qf7UQwSpA<57!Y>X%gvdI*$Aq6+e&`vEm-J_z#jXr_D5X^~H14Rcyn%K&11c%g zKzXdjUSh4=VCvtS?hud~MNa(poS31u`l4m;{mQCGgg_J(8`8p;%)Cl&S5^mGtR%K+UQ#E*9AV! z9PX0oBbc->lZxBoGtP9E25!Q`%GEw5)vKiU_e_=_XhB+)gqtS)T=njz3k+VEbMS4o zpNDPij#f@Nfr)UVF3mIh-Aou`Z6+I;o#qq={BhF{SmP6uY-5EU=Z-(NP&Pv zp35He%C!SNc@AFUyLv2%PSbhoAXN2d5rwneXduYy&D58XxO%s9P z5Cjh_*Ge<`uf0?(e03)VojE&{tD%!lkYp8dyD}~we51T1Ka0>9DG%xD)APqtdmgO$ zAwqm7HcBFI3&u=8(yy#Rxc4JWGMGNKC72-{6!9@sALk9Z6xcX$UR-1c8CW}uNQMrs zQS(ggTDMI1^RwL@0(zN+-h2u{PLlcfN4@s96mPOERu=}sW(sa#sk<=En8@@W7@M`5 zTjIkZ-n3FC$omJJkqsTP|V^BXH=QyXo(xW^4CuPLU< zNNSkR;E?mM`;7%1uB9Be8YG~lURNTZ!I4zoqbGB_DRdJCVI`9`krOFRARpcpBWN38 zVq;p1NN$wr(umTJf_@15#dV8>z3zj3R5AI5o!m`^Rp9t#?#Xe2r%=;_0Et}bwr@c~ z7R6L_BL>Sm#b^Hd2$=OVMpMF^A4;W^HI52V59hLFb4#Y2NZl)3$axLFaJ~H@uaflf z!to@NK#VT}9@;HRrD)P?S17rkIe@RPeWWO4IxC?bwED8I3G4Zr@2Z@hrY^;V>1riI zgFHjFx{CfM9IuLY*QT2|K6Of7Sbx_7-j$}{d5=u4U^hFcuXghsux3e6@e zvvM(!Jt^PeymHk*R6uTwS9E@Ainb8p!Xg9R-1Oz;=$n%%7hk=VX!HHgqez5}&I3af z8_s0YCHQ{o=!Wl8Q{<~&zg~}qZh8q)SdP!PvyeMv76B5q(M(MPAqiM;ZxW<|2u6k! zvwN_2AS`zHlKpm&eq&>7l^AHDTzbxMrTc7q17(Qe4=kRV-jgO5ckDCZ$CNq!nqLz7 zpveAJ01pWP!j1PUkymUIX{F0hf1rKi)O*NZoA`hMYXd*CdVhjHfSmWScCoOB@NdOD zDYd{T<|To~XE+%=hZ(c!xtj%y%Bv(2=9*6Ua9R_OwCkp#{j$Nv)V@6p2HN(Q?8S3h zPZV@u0xJ5mB@zQx#aMeCvm#kb+7?r}Vbbos^bC6@rG0s+A7^+nF|ymspHiVIX0%nU zL>whT?lQz5kxE?lRF3C503neErfl}5=H>@NpB{7|x_kXoJI#9Cefz&p@f4c0AGlbA zSGhq7O%XnLRWSJsBGJk=5t6j92(f7CDc&Paw`?oUxJz#IMcQ{j9!;MYl<_ox;WRxn z`OYbVEH~+0z?rd%5EOEdLZ&p`Ca$OGYD#B(jjkn6^>7hUG6j<^Go>EVxVTd5&<$^2 zTM%CSk-?VCIwMDjL~2TmKRmsM8067xVq)lT(ERZ88mG@vS%URrSrYxJG8l#aWw)vO zwbV4`<5`N3vzP9@K^2AmLDP(-LPY+-=$fR@;U`(kSJ_|DHQA|ESl;2Vk4#s+WG1S? zrM{b%dY&R!KApiZT~u_?YQ=eHPoveyl+Z)bVri(;r~eDaH+g$g7WSrxa!@On?61F0 z4Vi@Zc-}|yq(N#qRlv|n5=RFwXXe^-Wu%+2@cR+{)jErJlZb#wSu zVEK^d-=pGm=PG9`t1=5k2LIPNKOvL)h2Xf9u=tpv0g}Rgn^1l@`b$pFYmyiHG$xtnJL^) zHWz`nq$ESkyPudsHJBG~El(ii=H3P8u59E_>@SK-qotPLSJ6^F#`*EdyY|Z{EeRi4 zB`ZQ^8dO*A?j%hu5)dHdWs|}@RB82KCLRmnn((d6Q0X|L=P^>*n=|LnmjLj^hC=8Z z^Hj7P={oq=t4!KhN@a*O-aX;lAG1j}!vO+6ouel1nr|qjy)BS%_3;9{$NbC?RSH{Z zG1cI+csL&~&sKY4O)>5D=Q-kmfTO|F_n%*>INyU7!-@zk5yoir_dBm4XwK-4e zyz&i50CRT$tjS)z(<^;QHVNd#(xVoonn%F4;CtC3`;LWoQ7Ag-5AlW_e%X;|K)x8D6pxZv4WNF0mRtt#ZNBNCmkxL z0!K`+T#X(5F`v-bC=N<{~KXuiXw@0~+gyFg4Rm+Xg&|H=|(TMG&d# zc#@wlYoGIJ0{M4&fDSM}sEttuE*)dF!1}>vEJGo=7lpZ*tLq0qd*4>BhKWPG(&OFu zf?Dzl{QjKkx_0?1Py9+4uUk~vxESB#`YqJ(!7yY%vBMU|qL~>xQbvAQ&_V#=6su+(UgD1{&kw%6vUmg@J6i)$D1h1p04%k0x9JR>1 zeShXp@(rL-mSYzK{h}*Zx$2$pi$z7+F>TSy`)pFrlC_(R@AW}yil>oEM+<}Hvixwk zjcEc=BB}7|P&iLuxzckAu^bmzF3kIMsI|-eY`eIPwK4_en~_^KWuS&s4gz!Gs18u0hjN531$LKEY23bfh7vqQGGH@;k$I2mb= zaM8tkBXMuPlju!n_Qg#(Z%9rz;tnSb1P7F5VCqw@DqATyhw%om`94TAc`(eGfZ=yq zsLAR}KCa>4uSz8?sQdHN4j5uC?PQ4{x$K-msS&V2k-xuC#W|FO(X@u_4r&WqwR017 zX1n8l)>I}86jsEzO&(F(6LAhaLBse5y9Bmn)XZUx)?|3j{YokZ3Y~JSKf_9yQDq8Cl&J&u0 zj?!kQA zM#jC7u%9$wmU`~ZI8AGuToy3hqa2@VWQzdfGs;_K(Y=M=YV2X~s%feZlXgi{ezJv^V`KpJamj*(3(4m2HDysO zKNda`+Svn|{>b>yS!0ML!<;G`+v};WjWJLTvdhZSHHdOHTto<;SxwQsM2R(p@;l?N zCT*m9;5MW9Q#aOqu(3Ps^&HPas}$vU>4cM`Db}Z_f!f$r$n<14-(AoMW*GNlbzz~| z_31je7bEBAELzD`v_QF7FW5cZtMX@6K_(QY7gJY3JdrjKUDL;v*v6Tz+&wC!)+3ZA zt30x*Qr=^#8c}VJ*A&+KKu;rT(O;OEPf9Jre^YN{wsEY&KJV$&i!-fWf1)!jW;WZX zZ(Bj`Cp3b)eTs*TQo#)Jo$eF!0H>A&2GDN>fx(erc)Xdxe6W$`I-mz0y*xzn-VMb1 z1<1?SLxtIXJafaUO@=aB(Xt%^D|5X@+@VI+!&qtO2h&0GFdlqavdvbAvDTHw9zFD} z4(?wuonWt@Y&|?nW9jO6iCa(==DaZpjNrA5?SO@gLA>yVc*Icant5I#ByE2c@pW>& zpDXrptdueGu;u~ zc`@ai>i-K&GqSLaHTwK1?uW?OnZUrnqxGfleg`buQxR`3CZ%+So=;H(fmO={F$wBBKf^=XwE} zBl1?hO4^&gEXOM&O`?<#JH{ZY>F&v!FF$;rRSD3xnC0AsF@}8(<*#YA8yE4Iw*fX) z0~kTF7fvtw4H%xt{9^BeZYN_Q6G^yq>_gXwxzB~ThV=u2KIBU478V-g4oBX9d)}J& zW9ny!bbzK`u;!`tt=&tq{T&dh3t%ZLKeA9=sx=O>8fr=ZD)s}F>$k~khSMuZfB4SH zj#b#xlqcl(BG6Q?m}jIXj|+a`0tX<1$U5zP3&c{geF|H($^axRhF+F*Tfb_=YNF|C zYvaRjYU0?*^3gF5^$Z5zSjoVZ2$uCO?mYT#eyr#Fq1h7KsQpL3Bsm;yPRbT2&mTA{ z3m_FVv#*7$r@326-2uGC>a!s><(7Xx!3{lSGsDg#bgL&l;4JxRrFm@JK)lrok84$}(3p!MS7?e#X0^#|`XWNapI zH2w-wl7=puA>I(diuPDUN$oKDsjdHN`(FB=*EDK+EU9s!vBflIL2BQ)9>D$S0+R%m z3S-gI+?rKJrM|u9&w57g@1tK!{v5krP9Z>3XvI{bQg5^J0?-P+1uLf4x=%x4e(`n^ z#;X{YzAfZ{!_eJqdw-e|RzV-N@0=pYYtY1{G|6JzM&qeMw#rKq&Iw5oK6&VSywO=r zNlU%(vlLmm73AAYO=-*)tCu*+kv2X;7-=uU9fu*iV)_r|gm$5UE0=(_Af+-9QhwB- zx>QAfnyxx-LN37-a4$Z8^}C9%8hvj+r;dhdxaUuQ2z{XV^BoBHsq!@L`X%eG20f`M z_zY>9oG*DU?#`^4L2{^M7TCn1@u4j0`}CD94pnmm2@APenyt$phw^C6`m~oX+DlSm_hll)la`+a~9;gehOGO)52sWmpCGc5*=6)azg7#UQR$%^eZog zT)S;6RAIIW%4{9PuDxD841$s@!o09t$eZkW-#c6QDM?rv{Y{7~PKN}pz8LlCSZ{({ z;cW-XW;(Yn0jBq8aiIye$>spwoGPOksnF9dA9CuAKLB{Eo}XVr2Q8D;)j|bk`lHZn zHHZPF+a-^f5Jxc=LTgtlT@==J{j7?4TcmTP9YYz3S76=i_{d^uDUmA9=Q-2&3po@@ zvC;ww_4Qa+7xXWE9ft0dS*ZY(CE@W<>zczn{@gtCD`ik=}Yfe?|`)~ROH>Z)f z6lMkr!KJD8=%c4DJ8alIUv`C7KujCcD!n&*f;5Htx3cpqW;x&n*IZdtTVPb6y)f^A ze%wf3Yk=b=XG}5o)y0Z3ss9Sv<94*Od92mupcKONWuet3cvc&Qg6u$;DWal1 zsOELncs^(jGYXadz0nc%pSXCPgs2VxFJkf>k9HP==Rh#I=!>f4L;|QFjk$yvGjTTM z_>~QRaq9Lf{q*iZ@fbZNW-m2y{fscdK?QNhjn8#F_e;`*>~WZc;r8y(+a+lqv=3#^ zN(xYg$%`P?mYt7%uNA&)UbWd|0;~q4(_^fqh>!j4g@BTTevS`RTN>Hkn2-F=m-4`> zx^qpM1^!CSu3rfWQno0sTlc=BBuG0eMO|Eh1?`X9QcyuX_!l!QJeS>mmGp3Jw{nPm zJr1{wO$Ss(54n^Yt|f(PO`<<0qZ4v_MtLIWj7FaCn}EG>Dvm+EjGYfxhax|nh`|dH za(21r81c_Vl4+w5Gs~@bK;^S<}z@(JF z>qv#S(k%<$BM64`3PCcDi-jhS7p^-&5debhctt%5W*mM1OV4j_LFl=&YU$3u8BNvN zE6o;bbC`P@L2$6z1yYzuHy~O}11A2Q6b_S~yVW)`o*&)`f<8yRAX$&^1T$GY`2jmk z^ZhtHP>hzq1_e7o^qby%^Rw~1PW1eEz1E$S*ZK{`@}H1jdlotX=wPhTiYPnJvqQ(N znHH0igH06LY0^lj(k^UigTD=SpUKn4u8EZzlr~Kcv{w=v0o4?w)3{aSvU5hZp@Z7) zf;f}{s^gKcu)N?2Kj=R5YC4a3Lvx-GlxvoI*C07hIyfWXBLN9!YHpIaxcZ`>E6dlY z)i+z5UQ;850I7^df@DfDpM_G8lGFHw8pzVO6wo`{egV(nL)9&&?3|Z~cwl0%2y}N+ zmQ|b2&gk92v=a>0-w}=p}4O;Ko*>oH`7IZaSmra*0_33P&&mEFF zmn6W?{vhkP)h5BBZQ;k}`(5D)sVep(%vai0QCIj?s_=%rQ*-f03*nj_mTHtv3yBT& zzg?!c1CBttI%{WrE7JW63csy>>n2t&Oi6gew9;8vbc~EY}-^HG~? z0B;&;vi$CFKeqfpB9R6k{T=#)umkl(NQ7VLB|sRHL|4D>KRKaYUV?jGbp z5fg&}2#r4xT^(h*jRpew6zG5|P3`A{9to;k$c7D6;-kJU8n!c|>7s;HYbVP!uA6EB zVvmBZTl;?h$qMP3>`W0K2^?L1z?ogMqxBl0YTfOxP&bwSH65a)pPmaT(o7;qmy$6y z0Awfhbvf=z#Z-i8_5itsCMtpUF*0#1;i}m^03dv4{^{JT{7IVE_8v+(yH!*y%e@1{ zl&luOT4cCU7~S5r&HdKOn6;cXg0=*O`jQQ(QGDqZXzyYzIG@P%F8j{_E}|c6qlhbL zesgc?p()G?KR6lTR0(0AcWemy*)?e{s=xzpi9h>24n3*rn)V)gxU%O4M9ZyESFMMi zfwB2SKs{_02q~`&wrc=_)%oc^37wY%AxP+aeWsCt(`nY}%4m2gB^bVTi-WXkU-j@R z@H>0bU09xPTBzH!hYPRQb3_{_SW&AV^;#qfM+FLw>RyG$02*{XjC(EG3UU~Aa| zk83(~2wV8n!>1=SodWJ{Hf2>iKEy9JW7b+2;x+Ch>)(02&e$ zljadIE66F(n=X26>3*#4=Cce6slUm;+0r7jpoL9m#aA9i#t9#PyfKHkkPes3kX-3dwb$_>MjgN#KatJy@ftKqKwuiK|3$xF5CKYKK4?)}9Z=_o(&SeSdx zsYm)-Q1H5fTql5+lB9PBRpT1+_BVRb-yu|#&tr`X4Is8Z>bvn0zNlCrnei;z zrSGm@ccO3Zdj+eP@1!z%^U)Sxi3{}?GRi^SMqu)f#O)7Xy>~h_`SX?rgmyRCjpuFR zt`EtURiZ1*kqkjN-KuxL$WCX4u{7=Qh$w!z%OOO_^6XTVgV#+qi`2PFFL3lCNP*He z)D_!Ozo)^;un$%aF{FKV#4%FQp_rwIAB|5d2Uy>axsP&BW?dQ@hpfr>FYqG(t{RC3 zl9=AW93E5g5%0doVLZND8Ey8rBaW!GEttg(S2IYh*y#gLt?#qk;unY79~NX&tok-y zmfrD$kE38C9-GeZP5Q!WOQB6(eOgyBX9N;BRIx*AD%pEQ7Q63LRy2K&=-p(Wwwg;* zO@^yL$TB8w9{!%L+DG#!n2Q%CUzuvpLP3j=gsK{J(jAdRh-%KE&~+fuIP4qbDZDHx z5=2iqRE2TsXKqLB#Ov9mTagcQ8oJb!NNX{gMm|?TdRXadV%+3tir^ul?m>+0s0WBu zU4ph8<9n0JAIWD2)X<+2pXS;o>dmO+@{*Rm!itsGV1{lT6K6C>@-YU%`>JoNis|t%h$anUF=ViS}|3Z8Ag3f4R7dIC&^dpt0gV zPnrJ`mMv7HrySd2B75Br${#AoHyIgihE5w(&ZfCN{hV|~##9ke@)PRqQm8nM6tC?k zku7idUqA`BzFvpE9eqX%gLu_hX0|{)M0jnR9LqDhhY@OAbeUam)ILqYJqnd?>pyoy zXauZ|{GEfp#}z>?>NWOys#SAp??{q8i{GJzpUgCrG=0sDV-{+b{_0DgpiD-EwlFKd z!$^87p8l)gUO(%!daftr5*~acSftORH=qz?Hont=#>>90vwF*t?o;a}iheAtAGm@u z8m>1#%Z*r2$un&XaXk2I^Vgv}5qoBArW}`{t!#-`e_?6rnWJTcv(74|X%rwtYo%q7 ztc+JTd+);nKQ?zh9jA}rQ}*{MFHR1+^mXDzg2R}hTAAmzDy`zd_{q}trU7*E)|GuI znwRG*hW>qD@E)T-Vim@P3G<`8ht}eBFIhd7H1{E`70Gjc7FbH=|ETQ{BmuIq(uaHc z!%EETau1^QHd@m7j`~<#hrogV{?EV$bx?HIpi2p=EHz~iMz1|+k`M+5QCoh^Qdn1C z7}gG4hYsOp&dfbG8RTfH2>!lIUN(WAvOL9P;HM_fHRaUno;%fS5;<6`nEi<-fj$Vk z{+Z9ma3D!^Ar(9AR5%p#YHu8|0EoO(9p@q$KEH8xK4WTquJouRlC@mx%bEDQByV;Z>+T)v|`loq?cpH{Nw*5J#C zF5+wKz;SW%Vq#QXGvsr*gc{Av`w#wFTnQFfRmW?UNEYLAtOhn-4ig`4BB-`GftWU| z&lI4O4Y}_a18bV<(hkUp#Vv>>W&?htvN6b0H0iP469C3Q#A}ONx zyw~qOBgv42HJq3_XUgQ;C?T`*-uW1IjWdGYPqKK{=*2D!UdSA%mTF>tIFc1-mRH_u z>9!qh3vBq)P>d$_2#V|8T_mt9-Tj`os`a zleLTcb4=>mW$)j7VDfBOs+#!Nm(eT$FNnHLdEg(~;rg>3^g%48=<)HPbXm&q5v3w& ze-}s)iBxbd68S7%VDc2u`E!J*WY?rS;faYKOUZN44Mnm9MZbgfCol@iLe^|i^o}Z3nN%@&ey#4m&JcLkB?eSRspMm0{ie1k=FEx#^x!3P3M- zSwN==Yv_RYI1lBuT@Yoj;B9vF0f!P{)+DuAiA2CIyDU2$f|aK})k(R2vM| za+GBrvdn6F>+LR0kid5yU)kRiw?REYajz7!ozHv3pSOWd8pC}}hH7H7w_1R<8S?(7 z>1t};YPr<*Oq2NTyVp~b2U7{fN}%^MvnwqB`pcqNnYgAG8u^(B|9gt(xtS=^`gAkM zT?PnrL?(Vb{q%rb$zgUMz%5Z)AlI=)fiZxwrwO1GR9kC`(y#SawU-_yG$6KB%f~Sq zvJ@_8wR?(D_uHo^oeVbOMxz9(`hg;0G_*yLvhHjGi;EkGLNgJw>n z-#%~_5UJkIsF~utYMB43tLfpi_r z62!ZEHFM)bIp%|^2dz<5+6t)pnn!O4+p4U{N9w=a=~@E? zOx%928z1~s^47&fRl|icQUwiAeIxQ4S^N_gh^T9qyCTT&qqSO!OmF`CnV)@Q3)}Cf z+Co$JRK%ydgH~67BsA*l71^hi#b)8*Ek|;@0N7aZ^|6N&s(jw9DLh)rP6yx#Hyb(i zA*ci_pXErMZ42Yca{Zjp&DjS`Dy7j)10|n{8LUo-@R|c3bp>hJ4;wgE zgK|N|63HrS@0+66`ufVF$HkKEiknR=-gyqEWp38!zI_ zG<7_$_o|uYLN(os0iaH}4W5#hAda*hi45>C8c+1y0iy|fdmC!g6|sI4AF#&!nF@nzN! zqf@(!?Qz5(%D5eGVll+LICiKZeA2A`)sZz^H56rT^!g3zJoDj2sk6!}`qi)lQNRNg zBPGARc`47Q1E47mt^u^OAys9$w{*^YG7)+>9e38Xt^)KJXgqVjCAygF`@tJAJddygty z7R^krzc9Nguf){ev43Dw-@MSKIFd`q^r*PaNymV{2;YtZMSAzgz&>~&S?B!&y4_v_ ziH1Rql1>qVZya^;FTcjwI_aZzf)$uD5>6IV8I({ zdsNhJD_h^9Ab7;1i6!0*aHSS(5_;}AM@ zWF|Qc_v39P^R`&E4~$X^E7c5obcQf9k^Y`EA3Xss%?2i~66yPOkU3Df`j|>-u57*e z8_jn7^_$dh|3JfmFz*vOZaP(($mtEs*^d!YYI%Lmr0Qoc&okvI$rA8kGmKwKIMbGl zlVh63B;J5b}X8Lm}#;QVL`r_0-Wq{qAF^zh1&(lGq8K3NIs3 z59DauKa7TH{Kac^r=cgM1>3*LRKV_?YqpYyfj7;fK-6pPsjn3D%~aWgpb{I}pC9_oQP7Q_k;Z^9M{fr$KlFWqNpGtCx!I z!qFE@Npa()%C8LT%aCKc|6jA9ZRoSY0`6@mVEF{yZIIZ~a$13NjUL&kTy` zG?UEpBtvsMgaY^XeCD~{z*E*FWh|L)x`#V3g7ga00r9<&7f=9#$t?O^PDOdXM24PP z@1|1WNPg3ddga<5tDq?yIx61xJwf`K5_FODYX#*M2_-#^^x0~XMy^@Lfy)Mk?*4c> ze?-_>eV4e8(_UIS&DO=Rh)O8VHmF(6ms1n!_*O_abv z(gg5A3v>JjlOK^F-k<28#6cCsBLP4gE_=SkcK#i>3`8&FjWXvHh=S%)HLv!aO&`@9 zOu?OF?FJ;zH>u7Q_JzHhKwuulHDa86M=!%95C$}~>A((IKg#O%h5EY;sMgb-l-)Np zc?_q6Oo1_Bjv#xVRNhMyZ}`z>k4!4SBi6rHYSD%g-`bv^?x%z1LbTu-v@$u^_znDJ z?VWxtS5Nse_k!z{UDJ`wyx@r%P)d^NpM!Wc1;x4fS)#HPVCXCdEDWTHo#kWy=}HH@ zcVEj>BX@wvRAbQt?RLg=UhUb@4$4H_?ibFVC%0#0*k$!>1+w!`p_2Ff1?dqr^vxgT zPV1VcGVFfq`KLL!Sf~I$=|Ik3F59WV*?~3RVyw(+@h3K$3^q&gi|PR|^rLNFsR^ z6TpE32^K=!8u-2Dqqa!dZ8e~{4R|{h0ew)+tLB1YdQgm5DNw$531C)M0Htm`5(Hdf z(EM?hY>R%3A7!E^^OZ zaDcWr^#rba%N&p$QYcZWU_0>P_|6L0$Q`iK=yT1QzfvE%73cAlnHDH^4n{&N{@@mY zR_FPZ0AhF?r*<*q3P2kIx$i4>2bm%^jkD#+S1ULf^ zFEj+Qe}+W_Yj%NDK=rqspkaFiN|9}Nm5Ju(SBIOTD=5p33Xrk(f~6S-Sk1U%P*%WP zoQHXfH(|-tq@QMBCv}*k|pV?|xkjCIZlt!u+DxN}_k-x9+B^ zeNYZ&3>1RwKq*T!k}~>5wi^FZ7nB&IHj(qh6VT!FL;b#0F?%p~o z$~AodH83zhLO|LAX-B$I6r@GEm5>Ihp+Q8XK_mqRkZv5hF@RAThM_~LfuSS@q<_z= zdw=(q?K$ha&N_dbb^h6FyOs-I-gut-x$o=xT&EA9WA`X{lylJO6DnP2+PTg-j??zX z%$n-8p{gGp09YmGvPJCLiW7v7l`Gjf)Wbpi@`A2(#dN5IBJLPa0v%)lmBq!-x7r)J zs7chhJC|<~1r+3j>-n}QalFm9d&hfmx5aj~BKkZ@qhEkwe=TiQ5wO?`hhJxSPIa)ua@sY_ z96IAVQ9plk6+<2tQiN=#*7~}7_6Yn1OMbBXw;yF^YTA#VB2w)?RD`@QCH#K8mS=RQ z+k0xx-+Oi7Ek>YNBeVK$!b}cAxz|ut0{yI;>6ByhDB*$J!^1x^{9mxmneSOnd`oGTx>-Nmze;Js&y*Dfb{c3KAviNl$bTny=2 zyC1)p1sxHDHCsXH!@4}3Q+_9(lSZ`2C6}({UTu7Fd4j~V8VfZhpdQNO7c)$P5$Bb! zN`-tjGOs8XblA?n=>u9rP9lNU^#>8q)^ z>VAm(VQW|VlEBzpC`7mK&g0DpQ^_o~!uoAoqbJfRIOQHy-n-%a2J_bV3*D@s7USjm z!g|)qC6kJeJc)Pa22V{X_rpp`y?_bu&T}%DJmHV|C~{ht(58{ClgVLSV99u`^;tpr zP;M9$HroT?&u!!k`D5v9e?J-C^ob!z-IpAe8E=Bi$0UnPw{VS8;j@~R^+Vbh?X&n! z-nf)-5=Frr25Z3>gXz|QZHtPR4yN`74v4oqCtk*q)`9JP4siJ8pigs6$n`NAh+ZNO zGTA;7bvL(0W@dfQXMMf~(tBGvJ0N3CK6>EZi1)!B+4Ff28lYtzejd&US-*DszM4s= ztUgSK@;SENXphO}V{MIetsIDJ+2U~O6A=18O*sJ`yr=m)N17eD3|4PIA7fG$cFYIoNZT!;SGt?FQn|9=9v>31j;!}Jd$gVg`sRUtpEjQv6v*d zc1wmmUV>L_2;c{F7A$G4s^76ldHuoQxu_(e&54s7)T|roHAxV2&0tc~oz6XBLu;T- z=FK@ho`ZS6%68lLX=0^$PmlVH<>TB-T>3$_^KLCsEI>>(5r`|-FQh!IQ-hIx?Oa%2 zq%ja(hjGn*N~rIBZO1exV=mEjr7)eOHQr-)>a)K>RapUf8SF+m?}Mt-hVP;?l2YvN zdb4;11|z2htkKe4L{v8!&ohXLK!b-I>zG-;L&aGs$xlaS-R^3uIdDR9*SrgjCqkxs zkt>$sH_h>CtVY)jSMunL-o@_O)D#lrT*xP&xVTfyR<6tAbq2oGtHDuYh^(-F?BDmi z+`2(TKkm1;r<4v@&AKjdub3;D3tALDJXqn4SV~i;glOUHv^nC&XRWU4~o@w`eQvF`I_;_URMB_Kvts# z!mOwxCqI*iq6hQDBV`CXd90d31mv|_FI@;prpM7CdN0OzK; zo$&*&A<#st$#-69qFnAdND=aIqOxq~tK%FQHnd{u9qcwU!?>%-5 z+&7Uvs5KAOi##shzdC|oEcoT#2JeGXkP(v!4QD{7UuO>3bmWvoR_=xkf*lAAoxb`& z$J7y;$N){|Ww2pdn1h0}g*eXuko)wS?GKGoQcRCQM_170(pyO@={XG_%rRlqJM%yxl_|B)cz^{~h+fqXF-%w`AD*|k3(j(4 z3E*U10SOFe@K>oxilU%lnU?WG@$K2PK{zC*19G`*2#?h--~_8L3*4z1L*7mvFgh_sCL%DX*!uO1#=MUt1wcmVX}GCr0xX3J zX;PuG&{mBGSrifKV1iQ$*cHYgn{*4Cr3W9p_x5*P(R+eS-y^Bfp=rgB5}F9C10|}J zHq7V5cXgw(ndNTlgr&Jg4Vz@C`%Wuo5wRzbsu}e&cE3bCSE7V4A=+@9z82v0B~Q_* z!tc6@;lWW)G2q^rWBh%k(An0Mh9RK$Jl{ad(pCAQ+SQg3EWZe?c_yR$7RnCTucySW zyg_J0lPL<^Zm*qhWpIW@g6KGyQSlxyHRtSsv$c(WGTeUfJjB_0urqq7y%5Le107=z z+D5y(BYdIa_k}eDXAclpxE~f*BxI{yC=0tpMte&A93SX{1+iJ~vffy($6R!no_E4u zkJWu$d7+G!MCvQ#s}0@sVYuBgp5q2O-qnrYPrXr_7)MeIIht|2n)U{tFQyF@$PfA- zBI2l+$+dDjT2dF}Md3!)|HwUR#gFYKwx_*F97^xv^xp9DzPd4F2AuUacZ44BU;F7a z_%TtKzul8Cbt8r^gO+P&lDiD?7?FG?Gt=ymMWEi6>L$Q?KX9l7HM40j!R0c%YHK?0CH=T5M+_ zG7L0ow14bFxoM;{!XbX|qEn;-4vSQ3y^jG!?|q09>2PIHt270he!SZimv>SQG^ga+ zTC#NUyi8lON=;}DD>_LDtNtV(4`yyKZGX{7)`r0}_RZ!mE}AB}GKojFIOHyb z*-yn%@{Ix`A$}X#3EcNpMF>qV-%ljpe%7$_Id-6pl~vZSFAS!PLjFj!uz64VND_4N z8$VqDisnGXsQNUR4Ru&fUDp5~){bM*TZ~KjZ|*ywPky$!@U;EobqM+N^zNR&-aUW? z`f(w@w|YuNu?-~!iBJ@+&!rUHj*_Qqdbb-bh>~I`7kIhd8`aZqzIYEK@$ZfFd|nnCd#5IhEJsv|5UL>_-38yUhNF6O)? zZ4Zs|0i=?pVK1(N1JY(X%|iWxAL!c)L#wGb_8VWFsX-yXN_(u;a;_l@)opG5&|Q`| z?vs6Dnv<8_GkQSXrizSBn0F;WfBH+-iam!Zk0JUtV z&ssKWASJV}xH)&)i4}e*YQ=1uXKjE^59~9Um#~NSmZM|=1OrhMN_JkFe&S>;KN3&t zJ$Yl2X&Db>&j-ocWZ@d>3C7_Xk&F6zi494*dJDaF-7|My!ec}|F+jNb*U4$ED&?RA}QvoUEKddT759PD6QBkLPr-C%vc$XsI-4Kz%{H8 z+|%APAmFW2z9ltP@L^X}>(FWGC|MlE(b7*q;V#4-Jd{1x%Q^q>pgTM@=*L`la?53X zhbgYx=-^bI|Es>BjtdET*tP+&{|4(dyfIUI^Uz*S?02NIdKD{T>hruA=z-$7hTcnv z4+WsP%Vwp^CS6J~Me%xgV_?b&khuXvE};BwR@n!nLE0L4sEmU0=%D4KQvD^<2;0wm z`>ZbJhw5ePJHO1$I@?@9YSSdl;$?=8?m=lq1I42z3H*xkDJL~ZowPoiw_N4qeCBn@ zKeVT1Y!3ei1D^0-${K{Ixs1IdM8lejrB((Lr{3#HdQbj8Lc_8F^AR5e8tz4^%dEP% zx~1ju3x+lInG$vRWsmXv`VaJf3wZ?LeaX~$N95Q**gT4JFI#sjgQh!im1JCXYMBW_ z9q~vk^nX!%ioRo{7?M%orIIvblULno<*71$)c4_d{c-k&VI13J({BZ`1lQ#UIVmhE zOqncX0`IPtmqhdV4`J}WszE(-%KUL$3puu_M>&fb=0iTEQrv{B_j zhN%VXP4yJ_+dqCrQw3Q<|JGj5v)hsn$Gs>@t?_;blD-Y1nI;T4g`A>K`DpZ5bJxbwELHa!Ytssl#7KL^5_7YmQfZklS`JhXt5z zopfTsq@x1AT`O?t%^$n>hzU*A1NR_F{lB-}(y~}tkt)yO?%YmiuwG^F;2M2XtSj2NM+YhBgWznG)p(p9XdtWPdU)@!CwwZRQz`mtw zhh5KJot_~MqkbXG%S*dQXe=*XnET@&vb5&L$x3Wd;>Uy_CXQu&Z!Is2Tob=MUdW#f5-)#i58Y>4N(- z5!%h9(#rHsgB?dz#HaGCGd}0jsa+)k#~lNrbic=0+0C2`@Uk>L2Ww@xpw!zChCqv5n(flqW%AzbA)Va5yl!l<4u{EN{dM@Z~m02S!P6wbefateVu+Zo+6u2jb9=2C*swgt?LjQ*-w|1F|CREIfVUedmWtPac_r^u5Yl?YV)D*P7qTd1YIhj_D8u@S- zdfs@?U}u#wi+}fc=uUHggKjv#F(`HeNY?uI;8yvXp4R@v3>!#x>%yuT`Lmo|V;*7@ zH*Gp8?22v0)AEJvXwNb!KaO!(_PJ;!t?VgBC*GxP@Gq?qfW+*ZnTnu-|BI z+@%_~9C9E|su@z&W~4hvJ}A;(^V(T>m$)R08)I@L{EGH@k0y}ksmke6R-xQ#+Mn+yk#TW>19p zM2^dz#Kt<(sBFmPv);zt*|$e&;3ind3%6QAg{!G+Uq%YMZ;Eba5Rx(V7J{)DSNKj_ zNoN)|*}J~{YJCi~s8F3CTpoDZPFV_{fAQ^^&+#5njL;Z8e_?QV$E{ZOXvoO8l(&2Q zoQSYnr4!S$XbcI%moUW^Q6$V$;G!-^6HH1oD{oOgZ+8ZqDq+vN9Dl ztVqQJyr0U!(Y=!M<;3c>l-VyvK_;?#C%PNP3Toj=CviR@At{ridtW8P(*bv<0KOs@ za9x@!0_*bqgTLj@Nc2l*)Gf;S_ro_C|H{RX5kSnn2T7M&Mjgd|5Sa- zWA}=Y-*ik)eg1Ohu#Sh`pqhc(mk#t1=W%BPdSBgo#ZJ}ngS0%d^IWw*9=ff5$-0Ss z-bG(FkUGezlYdIBA>`Zmt9mIqy0jh|+xj>9p3@jwDFZQh8WuEvL=Pr{=T=b3pZ70n zXch)BB+Sr=*;1=`+OF9~eDrMN#peNOjMXzjE8#r|1AGiI%Es*4$ylv&8>w3~{c6DQjKM}xm*v;emodFJ7 zH9B$iExfIwQm5FaZhX!52QOcIe}rH`NguU&L<9n5vN>kdzuvUBDFf>En~U!j)y?;u z`aA3#+JT^3Mfq)JeszoN+?xAx(zaTvqvQ1?v5J1v5>Lp=SAh_AzAw01+a8m^cV_5n z{T80b(B}l{mHF&X4H3}rH?b4Nr(+#+l9)cRel=q+7fIO(#}#>QJ!3x-tMg>8bR4^x zJ2|*WqgCMDVG)u>B%D6n!6F>5V~aGu^Ful8N8bm*m@49}A$MUv{IjYSr^tStgNq3^uaMihNx zK2lxJHDi5lv7+7Xsn|?_ZRQ8blUd&|n^P6_D;TTg#0sUP3hx6i^ZQMwm1VTbNec{y zmL0ZsEb&VDiSTigEGsM}%XneE*8+}9#QyC%-nTw#(SugYQwiDp!Ph_Cx54d^wT7=j zj0biYG5?6epJ5xnFhNNvy!1{_e;M>sk~Rh%Dm)ql9&csf#wEvDUoLND9$aYFXn3_{ zC~#tBs}8PYnZFSz!Z*^VZ&WriW|Bp>+EBx?EH9&DQ*MV}wrRJ*j?WHR=EuKOGe|zV zcPjlGhD(T|&=0TPRby1KX^Zr z;rJzK*edYE<&5Msy-mq9-U|3s$$f(Zok3yPUe1a zh;-HvmW$mjb5NYYWqZBzE^a~8kn)M|6&Xx*0Y_|lMy&R&tmZ+(!F~fz2IY^k!2Iq12h8X-?jCai{u2#jl9nYW1;DP<@*tgtSfSvHFJOzK-v@xS&qr_HF=}|q| zQIm(Y(U9#QcX6O&pjnvR2f-pj3HsUfg$Y*cUmWf6!DnB9XyWZ&oaLJA#^v1$JZoyG zCnOvE@^rR9crfVgT;|@k`D`ZKp-bN}B4RE*QmOR`+?PK$tn^?hQG90qUhHncke!D} zg@N~EyW?8HK{os-DnrNiKzh6HgMk7z<1E|d3Y+TL*R43$!Ru487urz$YHJ-Dfr8Nm z6(j6>+r8XqASQlQm=`i%)W0Pcr;z6^QnuZU;r#we58n214WnV8WUD^TMIG$UdUkxL zL1?Q2KX)ANF5M>X?Upn2ILWiGW$V zt3a5!_>b@I!d+D3o>;`XCf?R$>T|?gft_uJbRONnbgW+W_I|xc_@@T`mJIJ2)^LTL zDy;tf1lww5d|6$fO-I94%&>ieyUFI}^Rox9cjP#`l@|%PUO9W0JjS6!qHiATF?4p; zDM#O*j->{+6Dhpy6UXfjb|}*wPfUX^h&L%1(a+xYenvM+V~ZPEoTilim*n7PB$>p4 zjBx6KBLO$=&V?H<;d!J^Ms49$NCYY{wOrl;$NlUDs}p-;ELj&9N?Gv|n|o2N;A+6^ zWB%x?Vg4Gz#wbv(U|^Vjwr)F9F`^bbrLZu?xSRF${CAm5CK_e%%Abb>%Y2RuT1B2` zJ6d&L?ADMoE<$F+yU>g3U`g5k`rX94W^GsjELpg?R>HO7fo>YIFqW%rgO}479}(xn zZ>r5u@kmqO>ilX^;k?goVG?`u>kZZ#*W^(=_uTGT&cd)aik~kBeD`N?71`TCj<-%6 z|ImRP)Ep=fp80J1giPATd%Z5rVW&oXcjX-2J^IbQ%0%%xRGe*oWBU^t+qioi!d~Lr zD@v2AI6hQ~T2jFfsoz-+fz+YtF@wTdw$LMYy1=&6GB@tEJ}SE|9#vS0i(i*=B5>25 zo-<9#lN4a8{USxv>uJ~Yo@6AHPm`ZRo3IaQG%kyIY0Y_7)!-^YbZEK=z~t*R%3eY%dx9Wx`}T>Xy?Np=nN2n69*azzt3fu7ch>bqq)aBnTMdO+cWaR4X|`t>f<-_$fTdUHor0@bnT!} z4t_dh{7GpB``xB8$CddV^GVu_+Vvndv~8z|u)oZY?r*2IOq0DYGxXQi4k1|2c77$w zfitOTV$;7w=F%1YFo~%snbBPG-dokcD?cGP(@DE3|HMO?OxkKtkVYerJj`#upOAUR zC^MiQn-!EEoJThyZs1}kCguLXsqqcn1phd#ic&zf_>4PhW}mozXK{U$Vb^kzjb^gB z0TDhfC5*BdFI?c(PRfSwUC0#i-gD;njE>O{Bm4_`aznHu+6EwK{1aoW2n3STLkzIyiPe*1&c8og%ZUOGGX*~p7A_FpsVxHp7R+P2!;#0`yvRiwqa(`W%4nVim^ zjqR%|62XCG3&m=W^-g`f5X97dw}tsy7|HPEI%l3ia68=h`VVfJ`Vz&bnKAo?5^izdJEv9%-Rr_r5olE&{+qhqD|y z8{WAIw(k*MR;JMJXe=oQrShta!GLp7t%G`V)4p2&*em#u*RV$j1=4wBvmtUE*GBY} z31g?3R5g8LuHM~~Pdr?1NjSRFGfjweCjsppyD@GW&R%-KLBuqhl4r$ZE?q{*?}BL} zU~OO&mre7qqOS1Q;`rjhOd2fZ{`Wf>a~zSG@*P}wEtj``xF{N%dJb_@*H67^sbyDr z2AXCD(OWNxm4?+(jkcA@HO)D-e%)lTwK?lGWy{9a28PW#_6z;GMrAm`7QNiG0tZ#Y zEbr^I!p^UlSIY|OrCs#Pa=ym^*x=pl6{}Sa%*t%$^OOXKj@w(a3!t9W_aAw>R^aOS zU8_`x+UvUpQFhY&l=DN3c%|$DWa7fC4Iw$$c&nP<_Je{t|ibrXxFQ*6QWk3@~X zYPTB!!n+%E%7ms#!b`JV{Z5*A_PJ-ZmV#B)MVdcMPIQjgGt{0AHQGY6_Un7A40+WQ ztP$Pdip2F7txZY1K?QeJQtIF3Efjj*7ThL4)Ir4QXYl^}YxWr|C6^B0Zp{r;VuaZE zvQe(V5o;|^#E-FoG&dXg?vFs%YYWAGy3J}tfEV_E0a-(vf)u7UJp?Gb;h5rf<8H{AKplet^;^20VICU}pp zDj4qCO?8w9g91%qgN>8%+7_qn6FP&yG<2Pu+-ilw+NfPy^JlPc zx?OXzVkDyrnH%%{4Q_TAZF3*orQUQQqIOh+ZdUVnNunL_aR_O+|epA!(W z;y6=NzNX&S)woB#2j;BAd6g;GuKP~Ws!QCuA@fDqzT@VKw-;uOzFx=EMrpI{{mAfI z-&&%Mnt_m$7yA49uv15sx6ncAt_YXqwzSjyT(xCUbaKHIeBxBDDgEGw$%|>)-+OX9)^y#BYQAL-uPQpHmE2k#!Cl2HCRrA( zd|Us9(P-~ZJ`sr3G#5E5>VQAxkMG|x^FlAZ@vdfES!*5k zbX`Heq{)wUX6W2SZ*7g6SUJ^j*mnDI2#0A4muWIryydFvCEXg|wsjss*wocY_u9+t z0J?_u_^th~pTe)z3xO+Uc;NC0b|pNS(8kUSDRh9^HyOqG8K2cSfK&EYc&wpfL-}_# z>qlc^pCEm`z42>}2|lKINkwBRZuL>IF$iV5M~F@}Tc(+Ey^a!Ne2Hrl##=F56)AG6 zwc34Ok1~H>kL!y#gRbw85vBp%lNQq=erhsT{95g9Pb+3=MFr#V&K4ui*0?Io_+wR` zLvYvm{o1$F%fPC~wv1bQIaS=QUYGM@3y1aLf4MfYANdn zV`WP_RRS%4d>{FdhbeiN`^DQ~M7FKrYADwIQiI+ef8F{+x#?tcg1yfc%n8XHL%EpF zOBF9aeJdY6h&g!wDGYB=*uYYtR+kcjt#%zlh|gF(2pks78{OpY5aen&AxQae2(SE= zn_lwEHG7#Si5ku#FMV2mSv#I79x8njPi5#m(Msio1@O86oKQS+h`z(sD%{i`QXc&KJSvsIUDA(Ek5Wa74a8{t_@6Oz# zmrUQ6XjJOo^b>>?(HZ-Oiw_k6*s;RpIxB!uT2{3_!wKY@bL(g&?^4!UIj8Se$X;*= zpI;y8V16sdKwNswflzb%#&eURHo=nxm;Bx>tXEy;Y?X=_xR73~{9uyG7eTSt$U<3T z7@!ioQhYvWUlL62`ft{$WFC#qrqL>rOY1!eh6Bl93pq zof;!lx_A=&$*H0~Iqv^)eEV#M0!hiNc>5f0*ft5xv1oc>X%@5TJl(1$k8^E0h5+1s z)B+~JbX6TQAwwn207JfDG#Ek!>hDu*(mp@oDEH~nDwv3QLn-Zbyy^(AKa`9GCzvD zD0x0Kdc2oSN)%={5@nR|-yXX&i3GRod=B=^L>yJqJz=|5^AH1A%iGQZ^DK2Sw-dP% zr7T3r6TQ;O&A(D}mj3Av|DBy9^iOJz+OO0cw=G_c@p<*f$1F`@eb1-E*97{$%$j6{ zyHcasUb^uAvw8fbJMdXzWYnY7VAJV!D<s~ zjO=y#=U)GP(`V_ADWlDGH!}8(^l^>*vr5ws46vWm<6jf@`BsC_9Kp_exx^kqe~0E2 zI=B~9I%v#BAL6JvlKFhdgH;x1;~g`)n;Xa-hta^}3tZd`?q$*!YbsFKa0c`Sy2)h~)y zO2go8>qIw5*1-`zVn<}r3x`$_O_1m5!`3Zw-|BCSpb*+YU8h4%AitpEnni|qGW!En zrNveaKNSs_Eyt}i5>EESRTwBtqC(B76poFJuB;6`(nnb-eA5cZKeer}Wm3Iqb_s$+-J83em=U=dF;L~ zPP%CFhWqV9^Ci9efa9zC+^WZ|C(2Io+*+|l{#{PIaq!AO^spRYk1+B+7wk=a(3EMY zz^okRfeiCd;M*GcO_1;tf#=A1)cn39f=HbPWrrX5(oN#o6Kd9>$=__o?%m2=X5Yj1 zjI3uO5lQ3`^jID(()=jg!I0d)v;FV&92L-z;#5uxuiy69_etOSjs)* zlcJcy&;^|w@n|JoT5j9gy*S1bxi^u;u)brLAR%ciB8g&=8eMbfr3OhDL$;bpvX9tcedH4g14aUJZDl z236WT$&Sfxh5;Ipbzd^$G?VS}p=YM)r$$Vq9^4aXSl_%(%~n{nz43?NhTexsk2wKc zLV;LEhWK=vhF2+Gkn^6k$kD*WYkUX^QL*MA6)v5`JR&~20rIO2?*CnDI+z(bDDiKt z^7W0j?b{M-A4akk+GBvSDz>jNS;yeS=^JDdi;bce-6$xPuDG}Lf9xe=2#=dY{%P%e zb>CXMY(15S5f<_#LW4A9gZu_rR3=nn4w=(-YaDwcJkvX=Fy zBQ@kpphL6uPe*E!Y7?u2O}F%8YR@;Zbdh4Jl3=?i6e_Oy!^1u#?(ET&=EH<^_LomZ zr^b-YnXyt)_C36j2y1TCesPxp`(&(;Xyp&T5kl@~%3J~8cKo`*dIj1qD8itwqr1{i z?t-+6Jv*a#(}X~?*|#5Q2-^Rwzc+oB{zW)iEc{70YSeAE8@pl={hm)^XXS87s7XU7EF{H#ce!l%G93P+KKg#Vj%oickX0*&FWD z4sy zRj59!8)N_mv8;Xn(;D>`-{{z1d?VP5qP`nlOP+kycM$mlP;5jBbmli69|v4dQ|U8GX_g*Ou0=J^Y6^al!g)&Jm)3V-2^dRvRC zw#y;BQ5%Fea;5!+Hv%;g|BS2E?^`{@M;iPW-e|kWdlBG`68ar?U>_D26oLbqTJ6xO zj&`t^>OXupqY#icw6(t1yG;%;z_6D?i<_r?qpEW2!%B^@!y~+pC{x}IuVy<=nw8Zr zTr@Df;TYgY@6XF4H}5vU3UN?=ILCo-ksgl*+e=5^KtfakH<^ zyxbCF;I&l>VHQCajzV`e6!7Y}-YuP_C5Wm65f7ba0j2ZxJ80rJD76SQ%Ysk>b)k{SA+@WE z0pU+(FyUn7&+rNy2(K_}%3*TAsK~ckH?v+dJ;&)8^g(~y(GU+)pBwA4jEQdGs?iO*tqtd{m9ZIZU40M5jRS5kaR$Tg*Hd%M;#)HDCNWw+ zGjY81Iw8=k*!o*y4037-2(0loTjTp173-tRUi^@$)hXoEp*4P)?PzHt*!&bfYYHie z!l@nT8d=oq3}-Q_l64DJ;hPo@Ur0S-4=6AX)4MmF)xT7Pdt_YEt>jwR z)sGuMO(9bRaKOd@N;_nUjRTm~8=e3jqcqa73kWP2*Pq5^Kp~3$DMHv?LG{B|A$hoC z3rkk|DF%<5!;lI(3yOQ9bDfH)>VQnkF`9;fwPC;*$gT$fLtA`UC=?kwn<@3`1BGM{ zcqWNJ^jTTWCyap7*jG7fA>{D(B*TH`PCbY%&xF$C1BX2zH8T`Z)L#$TtQ9Qtky2pV z&I}njRfi{!kSNsvh3-#A5DKVZpR_La1Ju)#A>b$L0Z5Ppdaobf96`@E6LQ|pi65+q zuPwGfIVjrZ8PPpZgpnACg`cKAMd$bcAY1mMCc#Z8f+VQMoZ>r!ojo9mD!J$s1+Yp> zkKOK&!2s0hLWJ0AM0k2uk{hcYc!UW4zK-; zWUq*fZ(aCVL%S|?$|^VY~b;8z$&@n8a_7eC@rZ2zhnm4heBuSJWdvkiYkeEw z)+m}9s}ltQR3OH&DZ`z_t&1AE5q^A5ieg(+3q}RuBCGf5A?Q;T*4Of0N=YWLQ!}oh zj?C~h3hycj&80g6Irxm_?fYoWQ}f`yuJu4K!#cnP&CGWydjF;fd`5WpSjPS*W}TeP z30;^8mo=5GWK6+Abi#xp$JqyrM!e7GUs!}(2y4lb#R8#eN|4Anzq}GyZQKz5a1;jXFpk7;45L<22K{SvpQA* z7+|;NLaw#jdmRd4Y6Dw5XaBY4MwEnN@} z)bmQT$QA!U2nOWvb^P<8gHB4s&vcVhK6bQZzlD|ZZn+0)pU8_l)98xOJ;BexGt;Qq z9M3;JH=@^nbsz~K&e>bGOI&OwGms9SeF7%mpuy9HjRKgGYpn8>s7eh3&*(y%fFWQ< zVB{ztF#PEQxkBl4e;BCJOTJF2&j4$FZAbSJBrQz^nZx%GR^eI;QeTK*&9^ebRg++$ z%YFU;#QU&26qWUbSv>nJmF>Wo={#hfRV!QrS|g1eFwpnjQJsj$*aCCmeCPCX^>>`@v>Jxoc>4+wZn*SpV2>+_jAX>;7$8}TtA ziX=2ZJ4t~~)XkA1J^*7+89@cV;X1ht#!0IjrR3Ux*Hn@+-DgB_S*&qzYN#=g?C1E} zwoWZSN$cKMC zeLQi&eRN#qK>BqgTa5n30GpCsJpWQJ>wV_lV`>k+#Iq3z^U;wO#Iq&obA*}92PKV) z+9=V@7Ua~jTtYV6^32%{mTNxc2E9g0|GcN0Wurt0k94&Wa3VaS5KjW{S zlQqD#2kWNQhkslYF!2&>fT(Mb#W(rGbo}S(Ep2KsQG-?@4(z?JNe&%?TUFy+9^cLe z#@?#~yQph_MSgqYFyhN(jG>KB5?JQeIQnbLsA6=v4Xhg-N&!njiDKaaInH6Y(b&eHqr_mc)=v#o2{)qb(M z7Ap(vspmk*mktiBp+W?4 z|H}aEcmUQ0D%ya;F@D=mY2oBm57)jp-lqtV0(pl+-L;#1{z@{|UyJyh%Z8e8d%izE zn}b*3d3w_H0FS=+oc#S?s=k*Pbu9LapxSsp#p%4#I^HSfu;`W)xBYu<*=0`Pve^#s z3aiUyZsBcHeV-Apr{v8i>BKNk$U9|LuQ0yIX)JIf)WIjp8WF0`kmiI8m(8k#Ihu2J zMQNT=RSY%itekuGAo|(8cg-*_q6M63VRM#h=ff~J5I>mvEsB?b=vY@f0h9DIeo-!v z%{b@Wu%ky&Jz@x|0=6uGUU#<7+4!v$B!MD%RPwT&fv;}D2=2)d)t;0yNIn(Oh|`g! z&+9-sIDLPjrazW;Jfii3?sE;%=X~@5y_spZ`h{yk%t=ep!!D7>ql#7C03{2m>NWXG)Lgp`zSdP%qMIdtA-V@2N{Ou~?C2Xz#Ey=tQIP)i_8pW`N(l7l3v8jAX&j^CRPIKXtXJ)0A7BJl90MUhX%-A+F zN=&dh-zP%xo+9!MDiW3zA*rjPQjg0;(T8X14*%&G`hLpsvc~1}*)S-INa<%35tT}o zO-6Q8ca+5I*SzWxG0!7IkW%Zh*sGk)xtV`FgkU>udrK-k23dV)x;xgWsjKx@ z4iVD{HXpLsrb$B{rk9!YAibCOXL|1+n!ivE(K;>2Av*IjhiHA3TV2#D2qVx;qEk_S zmqUiNt5gE%xCuDSrkxk3u;_q8cQqX|bIxEN_ z3Klzqo`te`*WXeHMJhR9B1Y?eTTgzf)i*D^nlWnnpD^C&W})I724R z%6Bn$zJVB`zAM+Z1^CzCP65t;_>FXrDNo8An3r_}l2>X2f>SkQ7w7adhDad(wb9Wn z777$h>MV+3ajNpr@eAtq{hPn#+yt3Sug=^GTcc3UBT(!jC7&4xR53U|&*W~MeR2_Z zL1o)833cd<0kt0$QhFVyYjr*6q$PWdW0^3zo=U1fG@^5tgHY(&159M$^L3}Av}(b2 zqA{C>7pY@-#7x5iTkbtiAtk5XrU`@==OAxJ$LWfN6^*jJ&|Xw;NWWTVtE*e3HXLTFpr zk%R~aYU@6;S`P~Jn$Fdbf8#%Xs1gI6la!j%AY^$Ib@5Rw7f#d)76x-(?U{d|Z_;w! z-3l5M!DR>o{DT_BC?XJKc%cvJ+At>Y9K|gD^vuP4m^d^nY9AiVxopE=xcwAb zpgEh*(`I@&+(e`3y$k6exvvr*zB|iq?$emDO6?!M>5l6Ua8(car3wZdFT%vOk*!(! zUhS9)&&+c9M};TY1Un`*t6-z2r2f7}Nbr+N^rre{ssw|P zP(nPP^2+p27E51!l3C2-zDeqMxta6>@r28B?5QEYTH8PlyHfTTrLd$(J zW<_};RM?_(*zvxa>x|Ky2!6o4&FMmxCO+t`hRS(-F;;A; z_g8;FL9Cf)raKc>rqzq%|*TJ5hBmNiLK@E~TJxCTs#NR9v3Q=-gl zEJMZdeV;6|MaBQv?>Vn9o)0WUe^{Lq&?kc1JKjwO)MDYW z^N%MRQlWXmVFQziR88L;{;))g9qV!GaYte;()Y;yL#YONvrw74nN?QnPLfAsIVez+ z!MD`WsgV7wisF7J{FZC(TY*0qiB6e!#oFeQ3$#7H-Sm_1B9$Wf8leP7 zGD=h%jjcG-eIHi&e5;T$iDvdYf;f|`ta~-hpk(;D0si~HU^5&F!$>cF)^#gdfN&!; z1u*euOWMw1%@o}b(m71&%^5K3_}DCk4xCy>);l8=u*xBSSPZ~OcTuB|qMdm(D~W0X zV{BW@!qjII}g9o?+Jv$pYhPw5fwZXn+fUm zE;WlSK%w`!`@+iXrK&J_k1lGA*L$p`I;O@I!RnfW<^c)t-@nJN|3^rt$n<|<8qLzr z3wM}P+M11Ku=Wg-Q>IapxWXK$!2BRQ968b3N`cMwRXD}h^m+LYo%=PyUpW|g{c-iQ z+{{J2=|OqAW$#1r8N4glK8>TtT3)ya+bQ`aH8n+kre7<&iEl(*opJd?LsyUB+QS5O zj=CG?@1HZW2N01YDsWBjVCteXbw5)a5f6d`U>qk{9+biNnMml2+>8Gd%)#*sVY;Zy z_Oo_s?2eK)2y=L!PP}EI!*a_~nm3?Hz%DV?4?s+L@DRk*4uY8eY4(2s_`9$HLP{DS zR1f&OVv~_L5Q)(YMPlHuTgW{0$vlC3flE+?{9VkP*gp<>6p!_|;;MoKIE!R|+NX@{ zClc*+7PW=$hcT-mTM~8FmE)ugKqN*wF$Tau;`725{xn_mf5yYW7W0JNNgphW`Gq`v z5emGMYh@g+8QIf!h2qaN`<0QOF%5wsp+t49Y96=-?i=;{~0Fa7|-mkh0LJ)jMX{{HO~@TXnf zPX4ifH^wk8G1iphmp->yC$yD1o~`0uZ|;TLXI?$Sh-|P@Os?da=O=AbvptF52p9n9 zI{p>ncJWnk3d=P2q&&Pl`OGOi0-IW(Z)h#(^a=)#%++P$b~E}C68ArGotG_qk?*#p zD(zr+$#QId!{}IP+(0X*&c#0M(~@RFfr{xKcML-Xh~`xI?slze1iyI_|K&Nn<$_S_ z?_;r)PlWGW{bz3Pr1%J>+}mRgmF5!7zTVPr69&|OH)87ZV?C!i8qcKmUombaeEUlA zg~7^znF)jCeb@h^$XpeIR~1O>}s8=;K2s0e*1T9dwQiS3*~p~y*yKckC-e@gMg58#K0GJTmLZ7L>g#U=6`ER3G>S}=rrfG0?`tHgMBie&Rn2FF&^ za1Vc=_A$fD7t<~%)ETxceoe|AOAgXdx=Xv+@viCjX9HE2f5%BqX#N!^*$k5^n{x;r zS7Fmx_&L3r0O1_}znNbBX&X~IQb$>)FT5vx&Y#+@Cs{c;m)Ys4Al zHzT>$(*9W1UV9b;l7u^DFUetZP>M!{z5-3T8t3ZbEwW=pH$js26`o^8x2;xTnXqua z&^f0on27M&f$wa5L!y6(%JDdMsP?)W5WE(H7@}fC=0rAKYhH~^bFB(6)1a-R_M$7U z@@61x1B1O~Bd6FyKP>LU>8R>gt+5K);Yk$OAQtzg=3x+@=sumf>TYpyz+os!32LUd zE!iY;Glfe|S`3h0HE6r~vd?&a#p8W46v$2u+Mw}J)&}@PA_!HFSqJ%N9`Ey9ycIVA zNmj&jPR>9UN{g%h_L62SIkGbM_hs>Yh?E31sb1FclXkRk9>sd87VZKOkK$H%>jrQ=P*A2&hRNY*%16hp`#e05*^CBcx~a+k zb*q@ccZ7^Rg?h6!gvy?ls7$}t}g3hW*zb8-yyu_$)H z2C(V@g&KmaOZn5r6TKH&C10MC`C$$)wYHEqZOUfO^o>bOqYtBX|7UN|30!q0=XRX! zg6O!@EL&hqqF4<;K0TmCHX0+)p=6;_)`F)_|zpe|FCkDf z&5WSSS;%qGOq7Q~J0cTihm?^s)L^e$Kx!mS@Ys&zsJ&9mK9QU9npcAsZY6Y0n^!}m z4)m!vd^Vxs;F@4K2pL%4o-e|#0MlDEH0Ak;hy|FL8j3O+o65&SU_A7H*n8`!D!2V# zSP(@FLZmwsY3Xi}1`!EKWr0Yy0xm!lX%;Bm($d`}N=Zt0$bv<8_ni-WpR?WjcDtfg4$h_A!4rR(@Ber=to#Cfc13! zzZ2?Md;l5B-rGe9CoHa8F_Y~{VLEX^f{UM_m{^okysPv>D0I{L6hr~1fS2Ss*~xS7 zuoo5G$xulJIaRsw1^{{RKHL1PvocE=;^3t+g&>0wlvxy^+=AFCFS>&)5v!k!u%iB8ggrPEcDP( z6KTIl+zxU-P9`kYZn=mf9E>eE|A~dLfM)re1cm5T3fl&}g-|tRxpQI@a42CPkBlSq zctwCm?a4C88Y(gcY$y~*W#)S@HYXbKeib4uhZsRdn4b9=6#2~ZB1r6XpT!Mt2(UD- zv_TDo-Sw_!+81Djptjo#PC03!@!Dd&_4FF<-jHZ@I(9mVHL^by|GWlRXlKklC=*f; zybjcW8N&a{0&0o@J&@i#Anz=~TJM1(I|vWuX!yqf??DpsrkFzW7pa+PPb3wR2Qg<^ zrIv*sa^Ige+?i+?|Mmwc@-qHHBVu zYo*)A1!Ea(teD2i>7RCh=bU8}0{`tktGV5O+_n6GBo6E1`G^zw8PC|dFoRN*w19(+ ztj)x%ZHl~aJ<-ta!(f(^vDCc0vw6YH-8x3hGt7Z);j9Kr0|wN<1XTZWO{Q@pn>A@y zit6y;$+9_|1Rp)3$v(G08c*r^+Yb*YNZB(atDC&0QX!hxxs3m?T*R3=&|D(2Y0edI zoH?)Bj<9)bQ_+8Era~vWj`S|SKECjF5}BNg$kWC8G;Juh&~Dyy z(Xl5@Uts@MP+$S7n`p)Fo7ngWSaA^=PmIHk$r+Q>Ex|$5+0{k6iyJ6>R0+P?wzwl` zj@M^*GqY8$zt3(sJ@8xvmc=Q&BoEt*AQEpRGWp>-RYp%I#&W;c?}V;>9eZOTtY<@(QfSMVDFcw;9@cFcGU{It@<)!p`BTTEH=t zQ(oTkPv=JycNCF?I{H@{w+dVeZkEk&K1v0VZr=pF#?|AMQX6&ri=JH-!z~<>uLg}T zbK_d^lg8emTLGjzOq#PVVOA#3Ex89;uVq5dBu zvM~AZ2bSZbogrl6{z8udXj(pq8VKYaxqEUvC#EunolP+Wh%knYuXjniiZ+jzpa?PC z@am_2b6U;{ob0egh5OhuUx!j>U)w{yB~bi>k%#|}lZn0Bh_?0JpWVqa>F;nv%iVH` znt=m0?gUQ7YH)iS>Q%mGQ*%Q=h`}`$JdY|?p#a-s3wCRMl=*8Yql_5!A40}$?+e;0 zl?4G2dZZ5v<`qD)N&00syP0okV z7F3?Rq7<3b?!X)?zpYs|h0R|fKlNVTM>C#dR^ZY7B`E*f3bZ~|l9$awiQV5P`vpP6 z+AKu}S0_Im)DtVx^CCL?K5go#B(4pd>=Wl$m2MHaZT%sL&(cb(Z=&!Co! z=vl{B7Np9UR%)q^^=lf$)&qirC$jd}ID}fD?Bm=ZDl|pxrQ35rq1I3o{aWbLxnQMcI(HvpxX&~%|K-svQL=G6i4Hf(~z2g+<&w=f%<=5-YcUnkl9}EYSU*KYpb6-p$}nwXU!RTW8}J zvIof%4TN(6qh^CW5`}&m--OfNXm5X4Hb61PQ&MsGeS6RwJMo*j1&Bt$el%@*G{*yG zs>Mtg#qi^hIPK`g?e@&BM}0RxC1+5Jme`>cHb-iK`Vm_z6ks2Ihxb=tBsBqJzgcpg z+?lkyklttFgZ?G*n)|HSZ~ve_zWXBotRMM}7O@hvg(7Twi~HPFxk^DpIobC?JbRL6 zXaCRY=nOBguG%j*WaN1{hScyreh26msM7Ku4>8~>@aTjpE$#HrD=keyrKMSJEkovx zS_NxTI0nVd&x>-J5{H!+5SfkTlx?p${4875vVVtvLG2^VkznlZlJNhdeIz>>w2!!V zV0)NdeVPmL9~%FU|M;KgKZ0@l;{TNe@b7E#f8#>F@x16|z88O=H+3pR$#vUQ_b{Jt z_}1@0Lwl@Hm)yQURc2m2>3|@&<;{!;1N$AzgbY^vNb`Dm#1_AXReL?v7I&Ja)qhbP z%MLcGw-T1bTQuyXo{cnq=;h5@*oD0v9gZQy^N&ELNiR$rvN--uChQGrH%KE~GtMF- z1PjCt)dczm1voR(jNT$&eMfy+y!cvCL-jZSsmnV(8)Cp~_`8jiaKVM$tmfrm5b^)U z2lMn-9}MCjXDJlm^$HiVzXXLf|HXAI68vwuj)dRQk#ZMUArv``6I2HT#Wm?-g6e=g zVi%tNhwQIck;<&nFEE8P9werj42zy1vfdw`Ml`0N8ioX;I2 z?^1Tx&ssjXM02b{B^=_0Y7G8z0kaG;TXuNex%@6X$AU)ozY+FQV94vWWEzJIB&Ro4 z7`EycZ15Tlx9V499LH;4+Knxp3z-s3BrU2Un+y1k{83I?+R#F@<&(%W>>_;1{V&_n zzbA`%qw_XJifldv^6;O+tQFv`Kx4w43vk2*ZHi`;piNN#v?<2_?!2Kr?<#zqd;ia_ zLVFj`RT$m&z@ThjF(;sazf>!>s$S>^!tWTG(_KLLouF-Y-uL9mFF z--h)=jeWtC0lUQBdR<-M56nC1n=m%y7;wX3$v=Ab))+V5C# zfHJ?#7B!PW^}IHwf_(=erhl>$OVX+wJ_Xvh3WkdB1#Zw;c|VKnbD%wR@*vI91RN#t zV~nOCs)Rd=Kd9m__op8GE%WbC?&(*Yo_VI;O%d6iM_zEOMC2KXR0X2}79m^I~;68D4tU z>pN?`xMWD9&-14fCL`DK&W{p|p~R9;D|=l38Pc%Oc=`6@Y;Y%Q22+-d0La9i zW{aQFGn<4$95ZvW2AlQGSt$!Y_0HB~yU*ii?Gd5H^X60B+IhD+(7$PHX^yQ62L z{*LQFo1YIaGL~&dI|YsBjtptRvhRn}P#iMYVqXqCjZo%#Ph?6O#|G;qj>myoCG*xd zyE!xjD0=g%Rx3?G@1?137oH6H2M8&7V31Bq^u*``TqkQXnF04-QAV|whz zQ-A~w43>kw)N}v?ux%6-_iN~+g3Iw>E&u+4ihJ}f(8qFvQr8!qNGLRy!u@!|6eQxR zK;TuWt$$mHif9yD_4-#RPi(t&7!0_*Ao(~nw{kI4zt+@J3(TKAP*MbmNi0W>Le;Ou zu;M0vB>9C&v$i#+@uq;Z{l8V9n`H^ z_G_H5T>;8gWk^q`Kd3CU5_*oCftH~e=Q1p9!WtkAOa($N<|ln9_pdnHFpxnLI2ZEp z1kCLpq1!b1R!N>KYnfU@w?Jb(r+&-EdwK?%aZUiGYZ50yX_O1(g~8I9Wuch*ZCN%R4)skfGYvwmxT#R5518&Pu)4RQ>AnMKhn>MZ@0{LR zk)VvB55(HxTwA5p`$cIRz`O7P$!#r{d8ZJv4wwS@slqjud~pa5Y|AgTk3Sj$Z?PA& zC`LikU_+Pnt-%=UTIXq#B~X{rC6;`6@jJ-O3xQ(Kxq&1x9qOBk12ddu*E8?G7QX|t zhq|U7k1L4ow$MEVU}UU&khMc6@dLoe>pGH0+@jZ7aJ-9&m*o{y<+kv;Wb@@yWnz_9 z{yPs6mbZ=dg+G|xRTk|~m)Xr-!pj?51m#O6sFF2MH-+V4_Z5pF1?&%ISI~l!ljp0$ zaWI>p!-01Z#BC!26 z#r>eopff1~f^MIWfCj6gprG$L*W_(#N(llV>NBTU6@G!W#{6 zf1xs|Uwl7bp3;3msr7o{^1``ZgrJa{B)#(I#f76G-61-tpLEK!nvS8pOS?#|PjXnz z=(FgLxd)!M(`=BtW=`fJ79HVdVA0KvtFea8QT0P1_S>oKu5!H%t1&P0aWj^Ex`#Yw z6{3burb${L;kG08JMcm8h(O`+UQV+IZD7aHSr&IcSO9A;)7tle1vMLPbFrP}u$JqK zSCH&iiQJ0nI@;-!YH6B@ep{qRyG$TySQCOGW~N>Is9U3FP`9KRkeWN7Oyo zY`(dQKBfHD8jd78DA`_~rgYuz_8rX}ra(nK8~@nV^}MR?sj}6LU&M<*V=}epv>K1x zVFpQDV>2nHbtH}R!=9ETwl1{wSa9_xt$8g>DN8cWpqJSHkAO-)0avsN}$JOM` z#8`vG3df(!kj-1xTlz>s#g_^YTFk;U#M+xx-KK@wujq)1D*)8JBtpFt5BiKP&hu$Ba+_8-Yqr*#j!CclQpO-Al8~XkV11+ z&}SthJ-$__V1Iwuf3j#7h->>2j}tG^baduh$>qjT{_+5!rv0)e1rjLTAOv_zodZ!v zcOyw*r7QK*SqjB%-S7UD!4-zYW#+1UO~;XB%@xYGN{k=%#-iWM+%CW~h=1e!L`J|k zds%OMizo(!>fb(@!W5HbuiZDS4F`qIg$9ES{%`rq<3dJ-%Rss&yo%#{^_ZM>hjeja zvI2~O_bOg|1jYPHT1BwfGXL32bhN~@C2yt|M|smzX6 z{(DeWcJ;b9ky7^%UKLP5Z|wuOfNa7+yUFGXj&MZPH*BFk_Q+PWMob=1_S{(=;%%N9 zrjcpw!6qpZhjztgDIeR-ZD?oC#B(&zbor2Tz@Bbd@L)91utW7?cwXny5 z90otX#(Ws==->0-T&A_w0j{RHxJC>Efrhq}5uSuL9Ne|%3X=ZS<9o?Uw_u=}+WDk)MaE!^P z7Empun+~Tz$n*dOhLdV9fU74c{CAjs6|hWbqE}mcR!vXjH9fz$*qPno4pYmD+8Yde|O^!67PG{6Idw61$c8?-5W3p+gx1>*9Dbh z0cxDDGJxD>eH&c-0tK|=O>r=M8Pft~<#oTHBT1YkP zEz^L5UE`Em?BRTG`Svp)TAHZAa^OL8Rae$D@Fe>CG`RBbAz$5hqdQ1XbZ65%x7HZ5 z^tz!wrUr~vtCT+o)}I&=CyFvv%p?9UmR<)v-5d+mrSA`P4G#14$OSC7rY#$_uK<^4 zLuli4H8~%vJP{Gh@gFz0Ka|T7zq5Hh0yfXFK}G-ip*CmiOwsiP`tAqS@XfZg--^(b z{vT2P2T}g(!1l*l~4V>gu?^w-LxTXHsW4+DqC z1OLNqM(O?!@g#Ug0{!H9V8s9ktT-7;r3z!N0}4bn(dn4zQsSp5QlkA0J%k7j@! zPY}ocG>{)McKt@}_X;mKH)O(JM3WYHnk*Vxas@aOB;v|W(r4lZEml7*3+>3JeY~=7_Y}#IYITX0UFc-!JD}#S zjQ#@6Fc7eS0xrOYv-ZD1XwQYwEvytH@6l{j{R9_GL1HvnWcSh!$JXZ90yjp)E+F6{ zleJ5f-g3ojNenp{9>g40YWZje{vl*E&oqz0a$xlU$*?nZtxtZuToFqo`XvZPP3g_# zC2w%Z?T_08m;RZCLE~e;{j@Kn;jY0($jZ6A(XI-=0iQ!7)&)>#{`F}WV+?0rS)=Qz zd?_P;bt0C?{xADJIDFGcJS2{*63=t;>s9`1D8}~l%=qPa71lc=+!`P!pYpZvIr90G z2`jYAB6KT*WzFHQZyRi&7qs^T^kfm-pPFW5>B2=gdJ;CD!4%^v!vIz0BS9L{$9<$qZtoq3)<&(1WCZG(80-4;4_Q z;tn-lasQ!0Kmf+O7~Gmt2sE^4M)#RJ0b1~(6PQk+n%v#ipRtfU0-7y#;NIkRvWHM* zact3fvn`Qse(oNqWlRUY)K9rG%PMZ0_;p`Czn?nd16S%l?Ai`}sxmzBo^#Aifn=JK z;g34WLPm(8n_2Os+tKaQK-1{6b#Jj{`x}<6JEP=z9Ec;mr<@+H93)^cKU_8ui-9Or z@apWT5bp;mLMmv>jzBdquPHF7LnTh~=lT_3FVar1G_zZx{pDH>Os`4|D_6okUm^+0 zL)04L6|{Eq4`huEQ<-zuoDF&Q+niP%*cNLM21;_h}vkO<& z*MNN}t5l?ysCNB5)Nb4_CN*1aY4{EX?y&M;7ByMvhTEBc#!s`sU25LA6}}WUKopYa zx?;Wn-kSm_S-P(c-)O{l_XO&QcZEKnH^>s-{mnGa)qQNxc)1<724^^X<`bf*WR)j9 z@{5LOyYiJkZ9A-nh4tIdj3LMOd9xf3>z|-FQ7YD|6Z8iZEr2=w zJ4l%Vht5u6P!;}ua^PYS><|)IomtpnUJ#v_AMA_H)!Z44i#dD1JM{D&R80On(H)dA zZyPr3G%r2(-W5AVifwlBx+_AuiNb_Dwdp!KPnK+`-`TSz{S+$diV)_hP#U!O1;}ro z!bsWbdq;;*5WbncEf|XL{=K1DuWq|W+ga!yLjZ#@IexOr%R^4~cv+rX3<;=Ax~e0; z3ttU^uBkWRk|731zorggI(s7Oj%RBnmt!Q+$39kp#gztWr8rQ#Pba*fs_a<`X-~$o zxCnbIok<`Vx+xR}jO5m*6QfXyfGMyw%8v=QIf2%?T+K$rJ)v#0S>0Wcy)iDRb`Gn% zWe}U@YK9Eb>SDvas^;ZI`y=5%A^dX7I5D>|a1j*n(wU3Vu3EZXI$lvR08R*&8lWYq zIEA1$>m~N6(7_HtyiBO^-JhjUR-dbkc#DO@kawa9=L{IN`xWA&$0~jPmc-Bl4}Pl1 ze6CcsP0(1oOCaoBUenwTInW?>M|0?6))Assu9!@kGkj!dWK_ zbq~{wYWp537}K<^QdF4;U*t~>G+_kKqpes({w{o4sxd~CP8RohO$FOPbyxc zE35wa*sRPy)}MAqljoOstS{G<&qI7@d_}jZy84pNj5Jl?#ax6xYSmL;H<5c(bi11yc!M-&5?gBvjVZ zG-ul8to&J&P|u-TSE}q54EUMP;u8dni9!!IfCU*ue3VXtvM-)BuLp@Nb%!qpM+Yk^ zR!*P_#IqfPvoL_()B#7!&OeSsWes3zN7d}+(5KJIjo;_YhD5W(8(=5fEcY8 z6h}T@<3!4YZdtH`N0vR>-=Z4MQ$p>vdk`BM$U-kvYmW7`CJ7pfs}I@hT;c)* zSetgVCh&{=QcIisY{kUUm!I1%Ey#!+c31a$=T8X?0eNGtF~dV+JV|&XmbFLVjl>4F zTx@vt?eM$@wntB~)|a`bm2zU~UO=6J%c#k~(N^cQu%?{F*wojDG z7I}va5mqq)r5*vpzLG}k<1s!@R$2TTDG z6|-T{6|D7AbN!}D;xcgg6@UnoMifWNU5iV@q*2Po{3lia7yEzG%n1huLC?aK3 zAvY4Icn5q-Q6|lic_@kC&1r_F;ucRCG>37YNpOJ?JUcFY-7y;CNXF%?OH@X;wbivU zu{^Q)h>f&sRN2&uZR(e-SaRAXtc^mV7)m_ zf{b7+qOqr(HtN*EO!ClM zKjoe9Y`2_o`5uBA!hPw(ACH+zbG_e3s(jPSNI$JXbL&TI#WXDF#eMp%NraVUcpW=c zS!I!ZW(kuCwX4C-&LqcIUS5u$)s1Ug38x2XcA1fm-?Y83EhdiJ-|cbk`M?OK8+ zLY0uMp_Vsuw*`Mh1(+_Hg9)ybA!SYtZJYb1vD7Cs1bfpxO|@#&#>`Np{X-C5c$omr zvqzltE4Lp<2lDJiQlz@&N<*PNDE)*UibM98Zzqh%R|1YUp=br;{Wo*2{p^j|e*ReY zdat&39EmX41I-3M^Q3|$GxEz+@!^ZOJ6}Krg=lyhEUL%7u)BBLzhJrD(Z2xR+OJzN zlT1X=rCZ>r81gwM&giicZFs@edk7_lAh*T4AVIaYdK2EgR)FPj8!g_ILd*?*TKhAj z;({le!6ojB-4{DG*&uXd{%_%GQ}5`$ta%<=@yi0(dtR_3`PAP9M+izx#k3Dxn9cI4 zc;=s@30^PNN1KH>1i5M^jD?&}R?bco5Xsrz(E`ROJ>@q#FNSUJhlW@AU98}?Qm+8_ zYs`<+TEC1T9@sQruvy<~1Zg_96sFB`CJ{a!e}v@F^oPwRQ6ZJHm$w@fFw~?(n;bzx zBfq-1oa$#Y8o&B3=1K5m_@2NM5lrVMt>koSQ8-Rjn*1 z&a(eqR3G;50#rl)TYzfk?+^)m@ipn3LFF^NZ;20Lw@v!eGiAtO{k^fGWs&q`HOe>f z3z{nk19EhU3o=y?Z{=w(ez*Onm=-7xz#D0kJ1f}ax9V|kRf~1;)zPb&ejHM<$e1?E z3nTc*bvZ(`@hQ8jV8J1AmhE!e@!yx!A0zE2F04Hhy4SU6ioOMx+u1pg5T}A z{9`v2hu|9C^mMgxI`3s!i$x<2--6?r7y4|pBLp&eUIyM4zrkcH--%~jdr|_7tlP!> znu^2Byc1q5I>XF-VUucEU*dar;ZV&Y?By$DEQd2WsphwE;sZlUj^aByLCp#O(9MEB z#wnf~Cf@Bd+3JeDT#%Vx9^@Vf*AQ8d$v z&p5`g9n#(i7-}Xg`mt?T7GZe-RD(En$RsWvg=@{Qn(?Ek92otf8pQa0rv~!hY=}z# zH}}nYGgFCD(R`mNBDf8Q@#(;O;%&-o7QWGykVC~G3j;RR;J9`W0V4lX)BoJa8rI8k z0-}&Y5QrzC|B*JVjsY6eKRP({e?2N2`n-@r@jXC0cw>KYo|zCYi#!lk(;9bD@E|Wo z|81YF2HMAI-ItA-cV9-kEN?y=q+0szOU84Kzyb-)PqyTJ zF}a9LpY!3KFX=h zeqX2uiNV2APuP6>0SHJ9xc#N&p8=`q_&pKMAzzosSwFY4FEv*$h*Hjo+4RQc5&Z&H zfc3>i>LHQIk(3*P3eoQqD{z1nGiz`ZAS||r{&c>E?OO=aG#+YSaMKP@6^Y{pu_5*c zE&nA18%hn?`TuQdNZwk8`kSi&)~fMqQ=y6hIkq1_TXsXUZTsGT$n+II0pH%AuGFl% zmb0<^v&;+wPjwGLQmWjew{I7W0hbnQsO>rJ__k0J`vByI5LNwxdi?o_7T?1a?_GG> z-{Vn<{{NCv{|}Q=Ez2)kx?E}sX-T^UFU$xM4OE~4in{6FolR^WkGjGtIm{Zgfig=Kl6425qd!LiQc1tw#*IGVV;?hk`P9`a>0Mj@gFE zwOpSDeP{AZu#IgyXV4x5{O+IlKfnAeRPx%*v=o26$_&K@e$qdJ)Jc}WzNlguqj9mb zvAb^<5Zk4-Y5Vn0m;9T>^Hk2Pq`c;3UVS3COeqGiJrt-E`{s4yuCuoO-Wz4%i8%`YcOb%+Z#q^QI?J+eO9MA zFtk}vyT03#j-M3-!lG;2gK7~}T_ALCVd@=@vJXB_170a;kkCnET!lmz4j`&)-~b9v z95M7~U34aA0sS?mrNhtt(H91VK4N8=+MVI_YDZY{Rr1-V5ncO^ zur7h6?4n&~`O^+$S3N*Bqe=n1D{8+SD~N7zJWbTQ*~XsWPriLNEQDiImC1RM7fdOb zIOs_$YyDR5^0UV*pTAw8BgCbUmXW?C-6#{Qs%mn%Ex^^`*2C81&U-CX*^g7{YTgsN z+_+7Hm2$FYJzmW)`Q+6a(DYb+72#Xk6F$lONRek#{Hll16~Pm^;YjkN#snaUB>Vu4mn(Mn52WF z<+ePNP^wSi=BF(DWxu?lAmVW-br#qWBe<=`${m1QAe|urUdU6%S~kXMnO}byl2<7} z>OTb)kNr6$2cEDX4%o1sQ^}!DYZm_R-Kn;w1vuMOi`@^9_$KEg1DfdtYH=#gHbK9aYS2tV(L4s)Ej;O^P=4QTbk zmX7PG5^+$i8=mYO+w~jN!(dex;xyMD!Y1D$fyXKmEbW-mu3JX|x|^v=VpH#M+b+?F zGB@yH*{9&_@_n5P=0{kgF5X#|@`qN=mn+23GSaw-(Qq+}Z4B{}@0k4%ui2O5_9G%H z$HnxnszaTLt-F}FEd(^eKKnNiP>opZHz>IbFOCHK{N%TuofCr06nXLU{>_`5p-CZY z$^tYz5Y<&V^HHG(;>f5-_WF}&*ujQPm5^o=7SMH-D1y2a?o}~_5%s*fJ+Mn? z>l9>7N63jK1iix+yi3IeG_7!#MrQpFVtdgc3jaO~zg+2~`I0pF@Txyv?2+w8+*YfV zr~lDAR}41PtZ<%SnM-BQN#O|LW3q-&B~M@(HGEQ0(qc}Zorz>VWu&+*SYoVVn_5~} zM*dmB#_G~z$6av?H3nN4Lp>pq$uV3o&O*dM@-8(uE|qeyfHR8Pi&~PH+B$nPAH5k% zjG^sj%e9oNlX>nZOOH%?T6}q&Yxio_W<%WV6!^brslN5h|0tgj9C3H7_NuQScx-ZE zdFV>fYe{%c#X}Nl5DMMqxyo#CATO%w{e>1v&E0IJEHjccqLBy4I4_(rIqt20+I1X8R5Fo{mgnB>N@Wi&ohh zSNX1BXZ7P6VpveOfn(l|Bz`nFf~!lIOQQCJwcA$AiA|?hwAhT1I~;(joy2G0AMGl; znw)HavDxOeQ;Wh7wuAMv!QktPfA*NI zjx|H~%7-z+R@Tt$?FY{qi8(JbKD~W4sUGXqE!msQW!g0a6|nvgcx8V`Ej8!9e_!Sd zq3-y{EFI?xLHcgXf&GQj7mGJH#ZzeZg0ImxnOoQ*0^aqDTw(pXk}?)A7%aXi{cvM; zU|yO16s=6NW>MWa6+fwyuz4Uu%n(oGj`ZdN)lGVj7qn{W${ndz6wh>+!j}O_+xr^FYLZ)4ciIs$P; znp%#u?M&&%K6AUBiMSezfI%%*eDI_KcC`AcZdAS_Gg@QrVWHU-v^O?%(HFClgt_iM ziVl?8_9hVa5y%R#N40;?>b#ihRlN{cyl)nJu~8!Y{=JPb+rf61nHWL+4S|1R) zXt25iUA(avV$=!#e0Ew)S9c2i>c(nC+nji26&<)|ba*ev!XHbM!j z3JPaxk681B#^zi6s}=8~x)L=8F0%{TSg^+%5lT(MuP6-5Ycds2RphwPZ;HCMJO6(4Hh<}roY_E>g3^H=wJ2M<&-OV z4Z@e!t7*%G?d&>og2%pI;dmjuM?_QoUZD`;i(MI(*&Z-f5K^1)}l&~QeopZ z5ME(-9M+{_<9z}4Lnor5{DQMF!AIg<5n?DMx-~0b>Eve@S1+!+jl__Q@L>Dacj2|R zoThQyt~Alb?clx)wRDtD+|qI?qF*zf{dxA;e$d$v=hAJh8CY%QyQJBNYClu8b#3c6oNBf(FC&_nWmHmbVsNXGp2hM^* zJgOi1s0+m71b%HL$toNo;bU$-+jBpQ(4$_+HoDn$l`6|^O!R6!Say=i3Am2Hna)}g zd`FfVMjy`6!%pu{q%RRpLbvtF8pxdwzTeW{Z;-xlZ$|P$nL&|Ydr@#-x?;R#s9b>iCMjvr)!|{Q6n~7<@KAd5y1R?^-Ks zouDyycoRt8GoIK)4$;O7&ktd?hjDCHGoHx&40nh(X!IU+7TD!~spW{FhU#d%7@vij z1taz|sv>uXEbfCLv3AUMhc-dMbvnMI2o4SdPg^_1MCSL%Hx*mDWi~W8r{wAF5p&rj z4D5-9laiV5IT8YI;v0TP%eW-A$;q&blb~=oU2Z%ZD^Qpxkjo!GL^Lut_BK$A>}xG* ze=f?#M#jl2#$T7*obZ>k&O!3c79#Ohr-v5Hbak3;`-?>rfttHaA9e9`!mdm2$Qm^= z7VyXwt1@`8DrC`02kMI!r0AFVe)jt!sRg4DB6A5tW*wULxAr`ayQR4zaVe{7h(8NpTxQ{FDj z2pv!T<$U~OdwpaXj4Srb}=K~pdWAg+UwCVV-jFvwzKk`lquidBdOJ-Rb zezrk1tX=uhsPww}?K{cHAvS!|r}KDW^TtwnE`4o8e|%N{0tWsZ?_4gT*$T*)wrp{~ zYeAc3AWL2D3&V+ns|UI5m!{xn+?K^!y!;tkeZVovqeX@y!g4{6^xq%nUn63GxHTH4K4!A8%o>L34Ol4mWzR7CBSexocK{L?<0cyf+Mcw(Ctc=F( zQzGQ8HrCcw59~x%*E-?b8G#hvJYg|L^5QkiS14}6J{S3_%fxK*&C+OgwTxBRZ(-U( z^&es`V_X;a!~@Uakw3qMfG>99PwU@a@(tXY0qKX_yBDI~A6o&8!*qMTJJ9W0_m^Wz zP!Ypz(is~THS_?%aGE|&V7Xp*ssHj>i2m(URTZ7LIJ_GV!V1e;o-5#?_j$ZEq&+rf zU)q#mpU(97MD~HxdZN0xiPSvu2co5BNsi>#FA;3KD~si}$eVND<1ikL`g?{8aL6=G zWM#F-?Y2={tc%%7u|B^^=}cpJ@X-2W3t6KW%x!wzv`zhGCTP5s3>H+Cdn7kZa+oIU z$UD36{o_Tl+vBwV$^sZJqu8Fw-TL&m&l^sMK8{((@LlUqWjRqe zl@r=*?GZygwiuOXMH>l~yY7CRuwQyfDGKW~pL2K0Pc zhwVMNur5d%5UK@#I>e5|O==dgGsX zoTz>Z4YgR+8C;He_4ycCUEWd!wU$7wNE>HX#=!&!B!N!Ydq`Tu&ihQ9oxP2ob{Kzv z1d>u2)*Y#fAjW7o;!gC~r+UdtzbO|VCvC*M^_gmTRF>6)zlAZpN5Pw2PTTh(#s{UB zjmVq!4K7!xNcw@yj$H(xng^vbPz;S_h)cJ>e(3p3z2H@tSlbqx=hVpD18R`;GBGW- zE|r5Wp1m!d5%|eX*7r$P2&n7H1w7X3ZVfL=wZa!ebUJXrNbDw|#2%GDvti?eTwG}G3Ri;*IMeC~90@p3`^!IjB+>C|K4=vr@3M$=O7 z#s12@ zg*7Zl--2(?u9K>y7qm2|;%QyQpgqHHPQFF;SbjKqWPZap2F}<9AJ*7Qy6c!b_sljR zF68dZB&;d+y260CZ`ID|2&q@ zreJW`Q*86|t8f!!*IjB8cy7Fr&d6){`d~I<&h`F_aV=QuQ2H*TQvs>%w;OyZz$;qz zJ_M!3mmY~JR4$Pe3>u}F&#RxlN{zck3On1m@y>!`s8)t@px;w4#C{j1j5Pn6lN(fT zI6R}W3e2L6d2U?p2zC0yCE^Y*iPLWQaP#4E-z;kDi%CHLDbUyn!Svivi_^r)&d26H zj$0pdMhi!6!x9KOV(g%twuBs@3U74Ih63e!+oQOSZqQIB2zDL`uxfc7`EtI^F`6Dn*H%hY>o-;t7$*WzfsoVALOE5N5cHNPEU{464-#Y60Rw6Tnm7A zk@LE$x}a9qbXSStg-D+x)yWXQ_xGVvv4bjkBKPUqZWA=B(__?XX^n96y&nb1 zNUKkCI#qH0Cjnm!F=*NE-=@6#rr{t-X$)6KH%y(ZYO|$`(V(>1#B5eJqmxlxM&%)W z)x4p1K`*g)f<>x(FR{BQ-s}%d%?PriES>~t$QZe!q! zg}d(-C3L{P63)q1g{En3PobG!9tC>*5P@RTY!2&Th82L7^Fl6V=VM5nU7JcC8;!bO z8!1zsnE<&546d7w;4JU$RnLi#c$ZbrvK|D!^`lT6=B~X1g4S^e+wQpA*{gcvJENLU z=RAqeD!U}cwJ!H?OzsbxwswqOJ21gXoZX-2&0XEMjvf?;&6RKTC`}EQUb{#W$vUNX^hRloWW6` zN%8cnq~nTv$;i+$t4%$Mx-uLfY#(S8oYlw}i$nOPbukrCovwRtT@pWi1-KxNoP-6R zBgBU7vn!G^Y;LQoAf3SCSTTgpO*CfLxU4w>C0|jEB0Kt|@AiPiyw`-Yv!q=rhQq#H zUZVl5tZ1;ZnD?Ia&OO?XXto=Fb)nX=S+h(l9L6&DXwN+@fN@j_#$^9$RDS~Yv|6dT zQ@mNP%sxK>@3X`O1d((62oo0Lfx(P#U&(jAk#5K7gdw$xNB+w1k`N8_#+a^@3YSS& zW>hW6KO5dncJJGRyFvs45c!Y4*_vvLCeYKMhV13fzqa~b$V$#WrqN9u*ox70HF-!Ac~=Q#nMXY zU=bxzTPfNXgsKl~1c?qcCDY}ACP9U8A11kLtm4Yj*&{REV`036?$L$<2e8{(Suwh| z=Vo%Tf+9wRjJ>2zM_Yk^U@&$K#Cx9NA0^ zGeG)l*qNQKqu?>pBz=MFfmQD6B#4t(Ov>W-kv@1bLtkg5AAC!0Tx-bz2Y49XATy7$ ze-Is!NZJDJ-V98o`4obx&4?|H`I;Hf91{-i=xzk7CpD0pSkIG{;hws!{DUxq}6s%r! z*vFYAUPMR*X5B6SfpEllEQadieTQ2k^HlKldJLNBC-hcb zf;)X`TdXG4TJl+$V-t<)&dSkqT?v!&YD8_m=F2ht*yF7VpBCXi?fMY}(||t_o1E~E z8--Vdg_Dn+9&aU_kvl3tN6~v7*`-73C$X588jbWrR+oo zln2r{V0CLNX)rmM)%k+h5%aWNKbU)|N>9mGYy!wGfJ0Bt1YS#7b7~g48Kl(bn$~t^ zsWj=XlrYK}f0_(dL5xkH>&rvMCfMf~T`5=H2HSxhgLHq6%w}hYLn@|YUplWY*>Qo3 zlM#;1L>wFLft{E3-TYxc%A;F7YJ*~?7n^RmF0E(w;&Fw-UM$ifSSf|x)ia@N#;-Vd zh|j#WV;9h$#Ij1@86dc(>c_y?qc+WcQ}Ocf=dzSI(~>?`leeLY7awML&OCIL^E%}@ z_}oA@a3r(+*=z2{eqlEjJ-ff*teo}i`yZ0TG@-|IQwpn4lP2VUiT%#XTs(&UZA

    U@89qDPk-Qk-}n1{y|3$ay{^~GJ~H9(HLn56vZtA`aQf@3?d5D?N*y%! zXRJ)jeeQEBmO`QO;5ME|Ue$o8gSQ)aedI0Ljq{T-7!g6n?7B?zj^a?sE?9A3mm42~ zN9XaV`OBR_gC41%)z`)*4oaO=`otCGE1K`#-t{TSSf6`%Ajj8%B= zO~YrOB*Ww;wyzbZUOT!>$sMPDwoEzlb;aeOaIsjjbN=S-8gF1j=|9IHYO}qtfk1r+ zDZC{-(ZJJ#&C*X>=o>zj+xf;GmX{<-ozLu%@x?i>svR{o-9?a|x*r#!A5=r@UwyNZ zB3o|U4&tL+Jf8kRS#}e2;t; z_;(Uw!kyF3KMcit4P%e-C{G+J!!ERZ3}*enek_b*g_8RXztCrJ>KeGg@tXQ6Z_jg8 z8cn_t!)pu01hb8fRB`wpsoM8-1qKZe%22I{RQr5ze1s)?1-FF&vNGOT7pD-}MV9Ew znW4UqeABGKcAmmh=MY`6LR$RB7nrfoY`hpfW#SnZKp_WtAz-x$w4R>dE^53S2%EGU z&SrJ2SeVcZD2R5e5vU;5?}+oMaPDztgYGT`Q5c3V)ZUGZcGsI6pPy z0Ir3O*4o5l_TA4_b=E=gPHR+Zjs(kw%C3Nt1mnEt6ri)aSaestOk|rBHsHM?cmxkM zepkggXjjNuyfjbGGr(H${z~2H=bGlxdUcu?$+4aWuU7-U0lBbtKO#6a(^0zg7S7S~ zp+!sC@lFQ)sS0C{iF<3&gYa*UO6}2nyZL=R74=6noxXRLkt>eD1>$FTf+^Pf4wFvP z{lb(S*fYC-4&B5Z$UfDAK6g9D5RDK;XeB*sC0Ngl(dZtSkvkLu1{WFy#2#+S9B8EK z@w#GVLypRBfWFjLB@ftPG~N(H(vRBtp#0|pi|5ndf!0@}*w~g${nwL{^ZL_Y|TzB8m6eAQRR#dM6pl57zWn#17MmXP@@>ufK5eq6r?rw7ZSw?YakOKxd zuZwTf=}xSs%On%{xqHe4Z|oXqi5kQn=J+1xVDM?@gt>3+bhiEw50$J)@0f=g;m3Lx zFNH;MRlkyBJ;rmpT?!ySrVcRL`&qaKaV%gr@+SKX`9v{yA=IR;eENi?kN2ol(XnY~ zUSwU?O-HwdCp!=Su*x#FP?SHFzl(KGg-R2&zKxu!_gL}gb0zR9wv=~FFVzm_nJp&W z8tEC&Ze}!HnrG3|?ogdmpKxflUYB3|B5Kf4WSoFnQIQ6Qip#VH&cdvkhYdE4b$JhH zFIQ08b+>J~>?Ew+9z*YO@P8YjX!ie9-ru5g<|u`iydXXiC-mIZBxf;anDPx$DB%<| z<}|$1HI13kduw20S||N}+jMfkHxx{dm5Ey4FhBP~oKqz$PdoBR7eoePdqC?;F`E6~ zB)|AlA??gCC0{HZW9tVrD~WW?KObgeX8LP5nl;SnF1bmm*Vp5*jvN zl+O79dG}5hw7$K3m=adN-9uT0cT4A;jo~B;5a=^|&(993dn4Z^T1>}SKutu!SAW`d zpLfj#16N)$_y|lb`ylmOjz>XW?j zG(N76ZjY~RHCrWftKVQn#bpt8Y`*CDsE7i(wy)vbMcWQB!gfs>lDKkp1S^MAFHFim?qV5sV|MD1tUtgaQ;$b7fE?j&G7qC z(19PS=hj?o%hZuj``CJ-^XH?uda+H_)cWd$I0WRwwA(4+fp}?Gi$wDXkQ%$+%5a8R zQ|VME0#%k_ocn~$1YIv>_qa9H*t5{(*U=aOl^al|Yt9{te9g@H?d1 zho8!xSQ3@D68VN zcTJH#CAoFY#4a@XQg1m2f?Fp~3r+AFBN3}tC%y+y_VhdrpLXsHqF`(4(ec3ePbH}t zn(cC)7!GY^9~G}bxE*}CP_+h=J1|Ha$+!7C+4FbaIGZ;`I)Ds4g<2f4^c*k$~Wp# z&iEB+yKI$dp-h!g(;eO;qs94K-qC4dAIJhevX>S4vDaBHl?Y{_#+OP=IB%{Z|76Jk zpe*HOoSwu%fg8txHQU!MJtWt~NLLz|!b3hx*8O_O50eP_sJBz$c+o)hSH15Nd01}L z^TKRTKm(uuTVT{S^k(3lR<-X~D`yj5pihQ8&vJ210P6xr6QGzPtUd$+B+mMjY6sF}XA=u6qc6=9^D>>rB zm8!a5BcYmd@r&mlvuOR#{3`Q>Cefx)ZT_`o z5_LI@;!7>7af_<9+2*{EmE>G^&o_n&3@1KcB_cG z+ccXwm*qLM2w})ofoP*>29b_{HLHy9!Emurj?pM|r;^EyO?23CN{JI72Dc6f;rnb! zgae?Kkk5pppu6pMLv>@4YPx$)^Rjq?_K_~(?dtxTVQ*=G7P3@hi`KsEc!k7I5+If! zu@s(?^bcbc1CLHOI>5>QMDb;u7%uQHZw+GU2UEC>K#h|n+_bL#t`^khG~t{5U3bdX<~GyoaDij6nS$}1M{%?W2m|P zsxS#SveS<=s9#Gz+G28iIl}+|wqqlW6J3F@vg%wLah_jL++7lo)A#Lo3mDrx61J=x zWvuqBO-@Bx=*1ed1TF^Os4?2ExF;e1^4UIqYYv55tb<>tX3MsaNb?1cmu3U>X{*nj zJ=byc(_-Jk%!{3O7ML*`X5A0_gPPwT`{D13S717LrDfZmI4AKP-Q#oK&T1Q2U&o8l(+v2lmex46;pXG`Np8^Md(4_ik{)4#xq5~U6-JdY5a!(jD| z*g7;rn}FYafW=ABMTo-2vjvJ4TlM!8^mKr`!4XYDs@x*Y-4r%6z}?}9jpHqvjSO%( z=vD1HPsnmz@LGs13nSVpk1bK*J<1F@Tf3QPz7!yrexZs363zlrxG9-#6|Bc0E_F+0 z-C1?s{1&xzxQrD!qdJ&7;87OZ62 zY#dW-j2!>~ZE`!-+vA8<-*HrC;M<%OFvxkd^{l%G@BN&~a{i4P2O7YKSaptbV`XH7 zXi*Aa8KIw!Fy>&51h%|KrKP*zMNB4!U@4PpU#Rdc7hQ-X;1NEhXGQmG|J6hqtiUg& zbXf>u%Lbl>=*T2)7q01p(mBEy#b8ASQnBo{#$5QfF*86UP+%ENJ$I;Nwl{C%(R@%< zQZ;e7WjveKyAYlA6hK)0r-J6%y&l{ahXlh+8~M?i%UULTef#w_^x%gAS32A`4QSc< ztwV#Ffv6J_BI?!LkN1j862F9Q6|qVWtfrB(8xJ2?zDL_FdhI1_nQXNUOr7^B+~q2e zCT;=H2#r3d!!^uD$qA0?;d!JC2ob1aE+K;_)_D$sJZfRRk#gzgMM|s{@-Vd z`J?7u>Am|V7dU-H=$pR0x~l4CCNkgGmBeL?`&y>!9Dbq-lsM5~9pkgLA=nmrSquG1 zT8%bO&-n=c3Esa<;T9alQ#~rmdTBVg#9K+1%4x>>O0;o60}1FM>vVO`R%h0>g*ZU7 zHA0n#`N8xTSPLm#8h8~1Q8v8^QKq$S{!@1k{ZK$z+_HbCX@TR%w|0tNJRnZlUp|-z zh)z`w?e|u)`md(fE+1k!ctTCfJWgFiYl2^~Y(HAxgi8a$*FLwvKHmIipUn>j^g^YL z@jgz?9#l83xpq+5jJzv)s&oOGqi*(0QY5uD6LqLa60g8#uDTA+6FJJ8aW^iDRZSP7 zy^||ouqAS*T|0g3OQwE{NTkPE?A&E5Rv64I0Hy{dd+79x zYpVOI=(4*7LET{4jtRHiEa0bcXU?%#9*t=p!+t_3N>uMfd6Iy;B$Vp~wYU^$9pmBMOO z@4=9F{DDKGZ%mE#9Ev)$BfLgA^u^}g-;imDFk&rZJlpxJFD?So_nO!iMspLnCBlD0 zLZ?Tt3}bt@<^+AAG1Fen7oa$ zU(I1$iJbmyxjo9JsOq!oH|Y0<-TdvPu9#g_o-#;t;jO1;5?nnz!xDJCyB*Wj*Qnl5 z0@ULc4J`Suc$nVBkCLdPg{EfeU-ugD1GgZ~f;$T-ho1IMQ+a6f0*t9TIEzI?R4+ZJ zILWXCw}V})#EUy`%puaavsmbBU6|XZ{hA(HUMGhZfNgYg)DuB^@fscw;(|DNj>rPX z*QsOc)xMo3CNSMZ3WhRLPjnaCs*U+R$g<;O{_TM%xf|Fj0Rytyv!*t^0TK!FXHb?M zz%Y4c5rLEq3Frz(_J?CNce9B4)J|nV6AQIVh(Fx(=nGqpA$EWF#(}P=CGnT(XywlL z0awWBcs8tu($#8Q29w{-91|_!btHjZ9DCFCg~Wq&KJmBjC8Aac zzD~=UsmHosEBj!hT6Ko0Y9#7`rTjZsWDh%2u}A^kR(`xYiHRGNG;YDo8Cx9*h*n80 ziK&uYk3x?S%Ux0!)I%Ekhc%M)T#`QUY2AmyT zKR;P#-{B(QIsSA^2Ij}M{C~Esv2=75V&eFWk!*PRP&O}=YJUd(i88~4$MpUa>xXYS z@0HeT^8<<1&*uJ#D&I6+uV7_zCQGt)gHEBtzyBEUm;D$O(!$5?v(4scH)^@R@ze0S ze)UCOOi}d+XMQN^;g-NV+%(db@jOgc$W{VblRc-kxm{28XeW%ESabSc50)kK=uX7x zbzvDV&rSK4S+yohZ;?@EZr$Y?iM7!&-6aB5J(YhTJT{6ogdu|MSmbAB`mZ!Z(9?$M$;VzBeZ$RJ(xgWAq%E;54Ehr$d;S&kBFxWj+~m zxIx@upl|n^a4}CK?OA9jqgbBCLsQ!G>Hv1mPx_==ZY*NQoE`teOyIhR6KOQdhMkw* zTe3%lQpK2~Cu98S>t&TS*uJV9bY98ZEzi|xjx=b`rQ6k8v5yda;)}#nR_MARV@v=J z;*%;L&JIu0Z=B@Jis$qCC*%7^n!rVcgNepX%vuG$7a0Zj5j`2{w~SEe`@+r2GOoAI zTpBF;kfL%EQ3tAEEB-p~k)Upogf)LtDXo(Fe+8Vs`rKd6*xx~lvklZ9YoSCz)MYju0?p%m0gWi`Sy2e{Wlxk3-2OCN{PGQ0Np5d z#%A7kcO^sEPk9MeNC64+51|pa!7b9TjJ38Uun;uKRh~QA{I?K zOjV+GaKrFTCVG7Ew$jg&dJ_K(Vp(uCoWP3Buuz%)I?aob|J}maPdlr`;EbrXHA1&^ zQYAj7@nqhawB+6&cVw>mBh6nZXHY33wL5>3blo^O9~q06iX>k&5&q9f!2X%}Fqy9P zZgP%Xi{a3PZkKq%?%IKDOAOPB|94a z!Dju%L}&saPTvKO*aiRhig{d9ha6`7{CNAw*=lXINSvhy9JKBV4$*eRXnj2EJj#Xk zMlkpPZX4j~_6TvFi8+KCNjwxJUJVg~wAqrSX2vEb80ZtO8~L>3c|xLdFR6}}Mal5_ zriZap3Ar)&*cX@`3y1m9{F9FKGip$#;42Wa&$646oyX7%rWf;6-wY`xJBQCYWs#fG zTy_(Sf?rfo=18^GFH}8>g^ct3?hP5^D}=MnBCZYgkRQ?2uPG;sdva896l+_{(!X|3 z)SThuAcW{d8+5QBhqYP=0TVeX{y4A7sJ4Hlf`3tG+UWhnc=eqdqtER?!LMT$GSt0P zm!s9o9IR>c+8J+a3y?B$F6;2(efQiy=kkSlz`f4#pZ;bOri~F3Q&~?(?Y7qka@Oav zDG7~cL^$g_rpi#2=Rk}Hx&HC#c5=6^PE+NFlHsno{|N!WImp2^AY01}Xl>ToADReth_k+e$Kxyy4pQo%@+> zC1mGM(ahQW#a|D=Nr`)I{()URUNdQT>()Y|_?(5rSKfmeU2Rc;d8}o&^5R;Oi^ZB;hpENOo zXr1M@hZ8^76jcQ7Od z|1KUGUYXn16pbk1MX9@y`{(v(gibCqq?x{i=@xD>uzW0fI>abH2!uv1& z(4V_Gt%jJmglsN^RgmO+4853lrhX!6^1{X>QmdLpN{7q(K{)U2EZ(8C^y13)YFw2r zX8}kzqx5ehm^l8&^-8;u`UZWIvY_)IG9|M>4M;f`UzR&QJB)HOMN&a51TbTc>A-)`{RK&luj<*Yop@WPtPCn#?*6YzsCpgSp2e@Lk07fX6Q zRLIQ|UOL2xQaO;?ithTn&rJW@1ybV&p1?9*?*eQ@IG%6No}1Va95Vu*g=T{KRSQ|H z4JQ*9n1;gmM-nZ&o2P|-cUioBDMFVe6s9dSIWq67GNLwSV|PaFf-n(HH$lNFq6_Rb z%%$1gZY=8M=$b;4(NmBAnLPfCT||CjW^Bp8{tlC>ZkM*s(Dfd1D~!(+a+hI9R?x!Z zEYpflRb96<_8hd6XQQ9^9oz5Ywz7-~FIs+`vI!Myoh$@lWrLYpKOvb;FgGE45vM`$ zjQ!HW7TkyG7?(mkN~+`OBhH-&<50-Ve-b2Y{PDVaF>0h<4B?Mj$SH|cF zxsJdh+@(RG#k?}KPl$(dco+#d?aIfZ*4CydX8e7zOh9s;TO>N`vU`$N|0>+(gDnyG=><7J&E+tM^%vDicP!-Ii$8aqZM2-KXc)Tm zuZ)0_>9WCx*MrU}_J(%E!~;5AaB$OQCQ=Y;Ar`*&8EiRN!N4V69~YZhy%{Owhb&<6 z+e0x-ma4_dpCJp>MVSJK3Z*NVE%m)f|9j$HBVLNolPtVu9Vl4z<<}^SG+dT>I~dn& zo`OCA(^SG}Bd;Z9ZmJxwk-1nz2=N0cQ};hm&A%h{8#bJ%#h`P$%pnlsJiRH1qFdP= zhRQfl)DkHhy@I%09>e`PBLX*E5*75@IPb(vTn9F}%L6bnLe&Ms8>2iNfIHT{Cd%l_ zplqU!Z3aTqTF)V9k4I6@uG?SJF695~zDiqiwQivCv3o1egbazvFDf4>I4!XR7{t!i zC&TY>8GWy(ao?0Dhh6qMB6CpaD$TpJ4mRFLw%A=;D*ers`f*!JI~>G9C?lqNq|jig*PRpE=J?+#?MI3$$KWinBlY;DS#{_4K}BiQ zbeCG5It0df^_abK${Pi`4+%_+h6~(|N(n!9D;3e-3o0AQ2>X2scZNR#sf%8OVKghW zxsz_tJ0Nv9GL;qwuvjQk_L>2MW&3j|zTt%?-f6313ZbUUsrCh9~iT zR@5C*l;j6|Dt%%T1=Ps$V5VKMnVZ*Ia-XuAC8fn&k4O4K48GkcC@fcf4@^Kj#Y#@T zvD(g1j&NLNJP#lsGjh1+ENnhTsokXZXZY+7%cd+%1;~$FrZeqXaYL|h{Ovj=*Rt%4im+et zMW%#-!HfdIWxC{roF7Xa=MHkjCXbF?&R?PxM3l=59<9fg7Q>ajj&wS%i=UQ%un9t? z(E@h=o|+0!O#lua!`Kzo;nY?T)Nfa3N<_n`1=3Siz2By)fcQR=# zyvMikbUJUjb!g5J_haW}Q#Y}evv&PSqMp_;iAYT}bD@x&?q{G+u(vm?X*1O?{w+yg zuq;X+{JOUh7@F;0gj1yJ(-FPrE2u3@FuKpPsx-GrkM=7#5I&o1 z>2;fzJDt>1U9zDs)BHq$bFG+W72gx!)HX7xF@m6IqML7V0`VRWhfFE^996=(4 zPqnCj4Iw!WdBC{YqXAC7Yo|TSwgjH8enB0uUx3&Fg|^d>~~SAoEF38#KsT-RkeDcI%C*>`kHE%iJmE+Ok? zzd1lEzO9kwYv@o|DVyRdM{Rz_VruPp6lkjzZq%q1-<h6!ng?sEx4yq~pzzNvgncR}#qXbfsTA(?Rcm8)2>VW3mF2OQZ1tIR1vtnY5 zO6S#dJ!TD3QH~~bHqTXH?67)0;E}!CCG4)zYItfz6G@>lLPF@c@=}`Z zG-+DhUH1NT-a+kTPr`0VNwoNqn}PU-3JroL^R>?> z5fPy!vK9&EIMGiF(xF9!^rhhDNRCcCk8&r`uWJ#Df#4WV?N!RREYTY)iB68RF0L6S z$4P1ay`^(l(S)SL(@c*$7uuZfB`(551qlOho}d6h1>2L&mm%R0d?u^w7T2)4xl+)c zgw2Pi7E+;CrJWgiL-;$uO-Wo}o-3h;jW6*K6bjL0KvY5~@=IMJJs|RM`IIIRcl0$7 zQNFEJxV>B()M^uN&}`1o-+O(N5)VO|AjrGr+rd5b9q&XEs@X}W*6j2#b}`}`r}G#= zs&7Z|wq#p#<=@w-WY~oyK4-~Em65zKjaR9MHcL6(Z|U8Zb%6Wk#NQiv;P!y)Hs=)N znJXRMo07@c{0x1K>#D5PDK|6@Jyu98UI)d@YTz&q{XxXaR(&Z5q*aAOe#(efm@Q+w z-NyC@+dE>lfE`GA(6fc!An{fL)}>f74+SpaLX9BrKotv{X(q4O{16dY91*IqZfz5i zfJTN;GRtBZTXKLPxrVd&eYpG*|FH}DI)`Oeu&A66RfI;;JBr1Zk*=ikWIml&f^b|q zAb^!pO|OyXW5>d4a-^>95A^#Oh;Au!J$tDN*DP*b2l(OxK0|D^vLH%koS=(%VQOXS ztY!Eoue_PyVsxcZM3?~DU5+dC&JW%RuqFoL5wWav2fvqw3%Un@L%4$WD8N*6!2ILx zYx2^;9kO98oB3VkJl^^!q?^Cb#-g-UmD%bVBi>0(5*Ol{%cu>F`GUbkYiSvbDDRPp zQm9C_!i7bN3AR1imNijw2LH`V^J-Aj&^+I;JsFV)m)heh7IZO8^rAleI|3$a4Z4jE*}imwnxPD-Q-BR zQ67A??$OQHF^^P|17eoNg1F2_HUf1_O@zfjuNa7lmC@yv;&y=@F&|x(rENE z;xI*qW+b`L3JvAt5rG;Q3?KFLj22GcBRG}oeq%H~z~|YehTh4BDXfqoq{mMu)$K)7 z>+9x$$#W<b`?$g-{c*H zKtiv+FxehTrgifi*DriBY*RyFib5OM{7SSM)9^noyGCb6U(}))&SmC+I7&BXWW^0 z>x@R^XjrW_l7kTEzO+#mk3Lk?@XeQ~n45Hg!mX;;7hiQgOMJxAntZ&`p=4f22U(CD zoHi`798HKZUO<7&8rB7mWpWL8GMtJ`APc?C7xnFlxr;>n>AgVVxTK&o5l9Y^%KhP@ z#w80yLS!qyjQO76pz4^+sj@g_MR%4D!i146G)k&Y{XM_jSHnCj>m;yDffql8+seSs z1*t;?ZBBEvh0WRFnn!#xY2ieKH|`oWa-y&Oj{|9FuuZB==*O@+rFnPL=gD7)W(!*pV8cTJ z;qB3+4QJO3=9B>-@E68jjSe%%K*7HJ^c`JLnl&a|mgGhK0`$^~sPo_G!=NBOH_th< zh6ITQyWA9R&r)0JXReW{=oW8cy3tneLiW``=*W)UinLzI;};|#Tc!Y z@n3y%c$?I#%5dotL5+Hc%_a&n%2%@`i|+}!8lX_%9z1_JhRhbE$iP!G>-AJfi!+?e zxm_#X_GXX}+bpR@ChILc2NI&b3|SsZxQnbbwbAUg85|v?0&Q$bmwS zwk(!tmpSKv%HZX4EQb!-B}M`fbRCl?pESNoJS=vRzT>`xC-5Jm7!jMso#b2EA{})+ z?~N-NIcPG}5826mx0fib>7UV*mJHgNcg05^$pnggtgyB*C30yR=t^R?^=$&y5q&7)N?Y4M2L`d|YZSA~KqV zw=J&G4FAeAl+xIU1J5fJ$uqf*%&;IGjCuA6r>P-lby$GXh!G(y+RGlXYGa+Dz(?J~ z0`SJsFUhIJRQF;R3$`i&Z5|I!X}t2vRsu>kd3g@83F_{o{who2NBj6pZ4Qy@Rvzu4 zR!94-Pq;4=ztUBv!<`Zwklq!2Wp5bz5v}xcF+N0+*p}DU;3vpj_yC`KgBHXJGAUir zxN}zG{Hc-J!cXmaz1yPs@iWNj4dx>ei$7 z?D$&(m%d(BdK8IY>f9IHLa@c)SMgJFz;FKTOOND3Gj6J)+EmQoJaf}z?Ai_UvAFPZ z*2360Qt>t=Z3WuSV(m+)UDOFzqHE>E($!02`NiHQw*bGBm#Lyu>jEP#OGS<3%%$nX zxeX3Jiu+s8pDP9$fr>IF5!yIo{<}eH`E)+pH>Lk<&Id^|wKRkF8BIy3a9;uz~HJ;>P`6hV%m zA7*dp94uG=lcw?4pzAg1%kUlcD!W*q^-6dypKew!rM5elAimN~vaDT_1T(RAIX;%n z%b+X~%R-?yV&{Z@)vQ081pvtnia)X=whr|2p6r$J0JF^`2LtruC84b9UPED8EODZ!+^ED~ zO#{=`e<1nQM>%@L92N|0!xU7ZFtMp+Hc9^c#TZrXf|vG%XY4ZmnHBY;Gurwc1v;>$ zkVMRjVw2iu7Vm5OjFIp>yiBbO#P{h#$w&WP0&c{|bk7Q8(h1)K7snGlBWqTh>X0F> zI;+ci-r6(|o43^f2$F?PoWJ~oj=h?`vXXHJahd{zm_=a_M(U#QVuBtzRMRV1I8`_( zXUpbrDnvGE(Uc=0^6&q1rxHK(R)f*|yZd2wtstj0Q#nD`NQIw$S!GgV8#W?n_Xn|N z(ut9;HK<7e-J7h$JE2ffW_f= zUuUk@w3DkKut%$0F@^0x0AUu!DkC>KFaJbOE z(pRHf_~FT%hFSfj4lGLSJ+<}*7-i8Vw#3P9=BD5FeQ7Vlk0{GTz+Jnj!&L8k!wJe_ z%OkK$%XSlL+9Cec^#S87%NH^ur1&WgY%`oD)jhXs{!YPvK@A_b6|rcPbePa2>d?GW zct`FC2o*jfyz>ii5Csicr$)I!gw!pOgIuZB@Dt-Fo>M3u_@Vez$(e=D{S8;DT+5Fr~%=muM8`t6Ir6K;L1vZcMy0ayoEUKAq z327UzPpRbKDpGA6GO98er&fO}5et$+>jc>pB0R=^HfmfI9yL`VvKP;xZIu&Z;Njl7 ztHUTV(EYJFVPT*3y|+lrl@Mg2Sv_ddx*P7g@Qx_!_3`&{esBY1isyEwzge}r7~ng) z-jA2^R2Wnw++ttk3V-_f{=k5FY)2>^43u}4;j{W&GgI1Z*Hzk_>u1mkefD#dcw|q_ zbfQCZCpbtnH~ye{DuO`!WfDhLTX?Rzz;(GZ6c!Y1)ZnrFyN&EuOycoEk(04f`E`FHsU9Ys)!bCOM-H=*9SuC>f06CO{RuGfqWJW$?}7S;R8 z^G*Z`tF(qWVQSL~3-_vyGl%SA_B*oF$LQm4>jfmK);?CYv_7J{(-`-{mu(pIEg-W{!igxhwT5fbdwE*~iX8H?qoX%Ph$RWJK_{Elk{;=TzUN3><{dhLx3`3VH{<^cxY1SI* z^D)ne9CMR9%sfVzxv$^M>fFT&CsPVQ&WH#q81yiQ4e6;bk<2hUcY^3cK8Kzt9B*&j z&Y{=mFPU^4)x9q6b6`WX)aXzNH27c+wX!I&7oy;@RuDS2H;6e=+h5s7SuF%r2aJ^AK{Fj`)Y|ZGWmA z3{p1{bXMHM_trYATrR*=Wma3Wq?1M&pj2ffgT*4;(d^_+-rcq3Ew;~pww+{na9U0` z1lO0R3g4fdFdJv~d`(C$E>PJ}6z+ZC!WsRd>v{#!s35!xe(=S&NqjE65fK7XF9Cyi8@PvdL9#de78YGDFShT=H{oz(GRr_U!JU%lgbo zQKHmd%|>=c^YfB7$th5GS-S6qqC39Op3UH&v1j?U%#z2pSQYbmh&Pesc_I5y#Skc2 zyE^~<71LO#lu!KojGkz}v@inj9VU}jF@eSgc&yWpha_XoMo&7R-yAyXaS!_Ia$l?yO2`8OA1 zh0LFu%-(3ya|_u{4nep>6@yRAxU15bhOP?a&G>ALBJVx#Rppy}=55Av+c5&AXduDW zXslP>msG*vP`Z-PxKPlzYARW=0pa>Zp7LXqVQ`ks;u1B(*idf-zn1PODF1VW4~iY( z3m>u6#kr!Gp6}O*mxwBY-@Cc29i&z`7GPlgSB0ELcQHkIIknTN#7oSn&>8y8KQZT0 zzRi;s-+u7vEB6U6?c-Ki>J_~iaZXFmK2sNDHMP3!?r>3?L-%`JzPwxdGuqxgk}kXM zs$NV@jsy1u%g&awG_%TJMMgLJ6I*?ER@tUfYD_85uJjjzj7n-~Ua zC3=!`;w_e)OEIx^4bCa$@Yh^GyczRKkt|@e3}0AXZI9r={OlmOPWZ=i)SD6)qX{Rk)ET}z=+skK1y_ws{r=J*LR>} z9hE-~9VS@+kh79AXL6uB<_BsT@`%zfWEuYt}- zs;SZl@@DVsAU`gI>t_aJ#ttO!mgh*tVm4Z66j6r=N*x;YB95O2Np<})JUsRSpfb~? z%qoP6ojjDh>`H8dg2=N;wrD<^{Dem@;Wk-mr{}hA^|{ax(CJ{Uao{nHJIKA_7QgD> z(~3f$F9~Cw57s{UbC+|GUWC@$#G^!qN7=$P1RwWl>g#i5G_tFft1I5FG4S<<#vP`Q=AVYqp23@W&S_bAF4(n2SxL(z|?H??P*Zv$duz|*1VQx-hzBy$fK=2(ofq!+ohPG*;dqm$ z=GF3Z-QTiCE9$fkX-z<{#|j-4Q_M)FsK23J%#VcRr`|EjF@iKVd&!P&qFD9+s>nSo zh&>jjnUS0YGXC{3Lm3~G201~kBk@ILT3Fz8;d>Wvi-PAE=#nuOKGbp?#@G)f-4 z)uraE_%eID7=M>2&r9FeF(V^9JWPn%t(KKF-OIj>bu^ABDt}aetg4?Iij*X4**dQV-~lC3JYR`LLa!BnG}4 zRCJ#qo+-gi%C`t)er{w^^4c5aIU$hEQc*r&UYhHb;d1U7$6|E$t7+VL0srI9mxu)~ z&Wv!2Ae(Lt4)h`;8Byh08ffl`u^yhfGkARcL>fy40Flq8NzEM=!#fir(S( z1TuvSya4|;I!gOb%&_RX^ih5+QI>+Ju4-zJ$D;86GlZ$Bh>`BHlBwnv5yVYX)LLvn zv`Bk?Y?FYsai}a?5EQwuB>DtaX?v0HxcndQ=|}NfD!^1~3*EPvE^i#JI>?IAu^PzuO_RpM`upz$6 z{+EJRx{2T0e6p!K^a^Hp_r*?=!K;w}!AIPr*U7}&qA~-;B2BY!M^;C_>kF;hR&>QL zTNNF4=d5;Xjk-QI@l^ttD`rNEig?*t8An~FKV#r85P_N2@AbfdYDM8?lwDRp|1PZ7$AHV+mfmZ{5zjFO>^72C6CDtgkB zs!h6q+}9PgXbrggVsgT6h38x+tYI~0SZDl2>CcGuXFUMiOpiv^0PoG*yY3GZ^OPc> zpWf`?Jim64)WVt_%g|D#<7mCHe-*!wWwJCzpUHQ!EM?0rlbkIGSFT}XL~#q)t(SF$6KX+WK1S#FaQ^+&*b>`JL8h^UMm$Lqm}!$b zRLcY?m~2?UrMgKk_<|@`S#+$mol|g#f^f$}r(li~=c)f78`#(Wh8_pt(@x%1ov_t; zd(NoEXPTOI*Tj@-JP5`T}|pgfXY7koJH9gmZr z$A)R|XpNPbMNf`j^m8Ruq|JpZnL;_ULL8Q5bFL?QTRs1o75t6b+`(z1zX+-8B+3Hs zi2;CIzFV(Rn`|l^QGUG{da<_w-(X zhw}pl$)N(M!qOA$);Y!AYfA-Qdu=tW$tpaust<)_+;Yo{$*wC%p=#~be$Oo@ySO6( z>t*`fV;JO=PH~>8@xWfkX{k8N@5^{zv>u9kNXaswJhbOcyKk&Kv^=j!0@-l)H#=um zRwgNeBt$Syq9pvhOJ6X|GgGJk(A&Y-*WM(L}XuY4#P7t&UHDa5LMKay0tn)FdF z%kjQl++k-8uSRoQIgJJ0bSZq?|A*}3-%rLxhQpiy1_+K+7fOoD}hHRg`;!PBi%vT18oa5WIcGv z06Xgl#_CSc`@$7~geKlti@qXiwg{@h`R{SJ6^V5y5|9Dwj~pWlj~cL zlFzXyHIGCIm{$v%asnQCC>BQcXj9j55_>?RicQ>leeN!<_wP{QI}#*;G!HSFVvx4z z^R5UgtugYcV8yW@UrVPK*+N|T(h%a2d-$7>3ze+sAiyUm77(ZaM2db(+;5}y>)e5# zQ1W|C464D;)5AuMyeqr16DT9X|IKY5bN|20(>h9LU)SsAcV|lWF1?>4FsaMuu(sif z{(Z=z@QWURHS#;R*BdIF+bZ)Vs!oi8CcbSUM5;qWHg4srZjOMBxtjT_o$Zdka2XeJ zQ8c$19TY#VRdXYFv^eB541M+~-E{^N%R6>b|7L$p@;@jouw_0*GBbozkf~ABrMMm{ zN>MK63Lou9mAb#+0$EjN<#JxWyS}3%hf=YR-*MA2Qb%Z>IfE1Oc}(JXR}9Q%pl$UMg-pf9+-Au| z)a*juxXeF9?g5G7Npbo6r+1|;ZGud7PRZxeKvlQi)FNU4^|12xFr(SWbi4`vHMIPvcv12pKDPr>~I9l~0?DnL4S?aU}q0RY-^3aHCipu7m@vm>+e6t>a={VAz zVbyK+0C8_|pZTliDk0?i5mUAqVMvR&v~iyS!r2~XcOVBa0O27U6<`iH0BR=tJez@( zWDSWO_+3EmyO0_n28#hcBg0tFY$bqxjP)^g{d9%GY6?(&Og>2fxV;DGSRo&*PUvNR z=(iVhi9)j;i&)!n?D%Nr)8e|@TY!ka9Uw5Kj#bzXpL;&%^TUh+t5^6#_A&Kz)20sK znI-1bw484JP_7f-ES2{HRKLjTz5z;tG^^6T-ppTZolyMC3=fzDIp0ybMRCB^NG6f8 zmdk3iO_TiQG0o2Aaz=Sv?6_&lRvSj)RIfkuv;wApHqkIg0~mEM@;QJ0r^9GR zpfvp9tb%o9a5%^a7vlsJH9gk>wvL+g;U^*_*31?zSd?V~Jz8F0=JWORLWs{sVd-WM zNyvPDJuW$^~2~Y!H|9FYW{>T9!S{;$EsteEDZBaojgz!1;;~iyvyDl`& zF!QYK`Z}QIs2HB%)X^Ti5u+*f&OndXx}@nq73+383@~Kk)q!v8L?+e+tUCtaAQn$I z0&1B^uZ<$Me_2)hSez0r4(mA>goJtXRWIwr9Wq3k1i4!0jbqXD=nT?_3)6q(q6*DB zgxnU!bxZOcq{`bxyd%X-Qh4#yK?=*Tk*kjf@8j76Q#TTW&w&(BVB zfFVYKA-2r8Q7|abeE0p>QoJD~L||#Zu%iD-&nE-d5J6l=*~g2o^*gauiMI~0Iz$} z6;PzLnMx9MK?_ztEi6;IolM+^^|RP11iG6W}U%c2$VhDT=)|j? z%^c}D-yBY(4j2iL1o**Bmw|Uf;)&}4Ds+F%O_z{64Y}5B)mTTHKH)iuvV!ihBYVK`*jszpm^T+gK97T3hYz)?zOhd?qFeLp1{=hY$j%= z7Kd%JrS%eHOf}=&zmFLbheSvn+b+BVRlTK$ip*tm&3gfa?H`WdEe;8^4`)SaoYwW| z0G1(eIFMLFC-WPM+)j)ueQkG98p&e!eyF6XcT<>oSU@2+r2Y?kZygo&8h(o_2NOX; zLRwJi1OB#6^4`=Iz;J|Mw;I{C%)$#4|o06x@+Ba z|G8_qT=>uUVLop>&))mlyX)hFvfBP&$apqvs&@gkG>KhagELnnJp9DeDz{9yW_$(o$trc0>miI)oz! zmE)Wr^y3ndOmoLRPQMS;a`b=`<%KN!ICuh{Ognif#651E5o8ffaE z?8~M|FF6^VJcx;_gXt^qEy%9RAcviQ4USE@gd=R(deP+cQgiC#Iy9TN); zqNA{llFL%LlrA*;PfMsNb9KB7eg+LuZBBonDO)Ljw$%LYuPD4fk_)%5;PiRVHq$5F zN$DqHz;Dyt6{PZ^6s1IQvQv#n!{L6m4>=zX>Ox|3wUYXI?Kx#b7r#_F5&Zmb+U%;N zIAw(KYi$UfBidX{zqDg;*5df4Rg1nr>W#9i+V(s79YY)nkdS>!j8$G`($bjO-?4Ll zep-(Y?vfMIy3c#5?0_F}5*=Z@-#a*6d82Ft_m0EcVJDoOJqi|;I&1p^!C|Mz8evCg zWay42`;{8@zb;03z!y6>9XAz}b1K^h2>*;P_4q5kG^eNR|1Z8&5F1}Qab|pJZ2~h| z8<9d{NyM3vO@AS0u-ydm0cNdJF81r&ZNwD}6z_NH3(pg=ltu^jbH(z()lWtvI&~V-_;?ZPw|hS(#bFX4R?KTS+1kkQ3{#K67*{{Y3j}$5LfA6ePQY4 z(z42jKqeUo`+V_P3C>D5Ad$2xgrB`o!Gok`8v=8fO0-v8)fbcthiIYN6AiZ2<77THZW(a0;{9(<$B1BS$v-P5yXr39C*q62wI zubPE%qR4Lbwp6{V$dRo5<=6%5Q2dZMh*I@HH5Xnlcuanr@AMuzqf_YEZ~L9NNgY9r z*Q$!*59ie9KQV{2obi2>T#;I(;8RvSCg*|WvV>Q^@uJ~G*eCJ@>)(AIkdbn~MJ4gT za@f3>@7h3$77bLvr1M{~;(um9*IkK@FGxh>avYn>#jo_G#pkYtSe|IwVl#wa^f z$8l#wzyfoFyoxc?n2)tu@M^=Ui9aKI|9Fbu{}z1VS4vmMO)IiBtY9Dj(cW(l<$-$d zC%9O0GC%ydU3^d8Q6)o~$rz8xb5>2VU1%<+-18s#5nTJLF zLKJV9=%T#Gep@hXATr;+V2_^Tp{3yYI`ke>3POxCGn+ybDZa};R2gPhcG0r(m(u~9 z5v36Stn=wqrdH{vH(E$J2Z`p}n+Ky#nkH>B$qWo}VJaWU$myx-pdU=Y6|FJ%-Sv#H zZdM%iY5AY_7lvm+aJxYmtP@$gEf}ry&fzmH)mBDawb3J-srR>05zd&wh{(?!6^cXW5oHo>8yTPfUoHO%Z4g9>eyMtF-f} zX06BVsY=gRZ2$cjq>MN;+FAAsc?)sQ@CPLIK6f~0j&icW{w{1}faQLY6mRplC}r<$ z3&Vq=p^+PFXS{LmE72YW{cv@bK_ls~2%nYBY=nv94qI|I8%dniM}_b$er?;%k91qy z89KKAi-jjQ)U4G`R+ePlw3BwK-~0x{RS3qy0}Mu%$%sM`)78?8Q&3jtvsTJ$aHdcq zyt7Z^5P~Sa1K&s3UNs;HL86h&^*+;&y-osk9Sw{bxtU1g80XFmmL!TU1Z1|F$~pRt3@2!r%h z4kBV&?7=yZG}W&-zO+P|8Q-a;%Bfs&GdD7kvvT|1MbAY{@IZ?$9A+{no80o>*Rd*p zjtw%AnH0kn+ochKL>peR578>5@ZcFxa4j*AW$o`EUBXl*Dl%ThTEbnd9{ zI@YD0*8jalPH^Y6PTxR~L5xpdmqWa>bb|=V=aq%ZIK`nFkWl%sS%~bG15s}QzX#8e z;M21b&u`)?7`5h@Mh&KBHdjI0jZquCPe~AdLPN>Il7&DSXBai}2B7*^W$C}sD4aC{&$eH#7FV`D(KqL=Uddhjer;h58x_DEPR z2wc&QrMC$}N_yNMg;BeRh_ToN<@YDIM^uG5=8}}%VUiEL5(`I>wZ=&^$=0oGmzod7 z`S_Rl|8_~gpZ$J3`_q4afjw7n3Cqmi2ll6}@_YA(cc%Yr7e&9-$drfqgbnd0Rg{ZY zQQYmzwRqBbGm0S)4$mNIn>0mwqnzi}I}LxnxQMR+2CtMr^cr&65b|eS^Z(R9;+aI^ zhbjI&5_nbehcO-ETk=1%A+OwcMwYEZf_HAVR8L6d2WaU^x7Tl1w1-OY&E2<+(MZZ2 z+PWqAD(&WjXKBFV%5A88sGr*Wm#3D;9Y#uTrFi#<U%7Kn4@PRi%-=y_CFScbIt9(MtmgaU7W>EF3)gVeG17GuW%Mv9%bU3@ znzY^m+Sls5@D1*9QHAS$RQT3px=~314}PEB!cOM`Pc=gAopoe_%k}qQ_v(PG^P~{E zx}z|y>j_YvaQh}+ebo=KcSy4gQdF|)f85azf;vq(u-BY*x89*gW+vqZO>gF;@u3ub zxfmf`H!J!J(qGKDJtEERtdgb+wAeN^cIiA&!*6cOtXgXQapwM$+d@Gt(2wQ^7R8#E z6ks$Xnh!)x#-epX^i|SQ3mzNlYP^4rrXaB*BKA&9aL*NNsyX9k=J3zjNINYZUC71w z_D(90Gt2nw2+FRKv4d!ZHk9AdjnHX|)WGQo7l*UX{{->=aC=N?zpwg(^6IH9skr(@*lre%_-}DLnjoIN*zR6+&=>J4t&Lqd zr|O^}Yfg2RH&~z8NiHP^Vsv4V>cVqPHng+evlgauc2(3_)g;2tDY@XW%E5K9OKeMR zdn>H<#o=A4>2lU55Wuso|gzJ_g_^K=E(auIW(q86VQ zqLR`O$%*{h_#>3_A=fvF(=teUtimF2z?`^ae8_jqm8TJo@QyugtCUw~T#ufhz*VbO zF9$uiy&(~d#=gMNT4Yi6Xu`!3)hvB~?vADX`Qly{#)Ft#V@^|AAHH9=BBBXv1b3;_FndGcI5YPVdwetnO)(}n)~nrSX<3TxJSs9oJt)JNbJpd#j^9CJ?r}rw$ZjXs$iXsZdb+FiNzY#;Y`av)wm`)(l48g zPQUJnRNmMcR|UPkasn^gmpD#_FNrr1qTP3qzZ{s4aqy7h4L-+@Kp%{0^#2W$^6&-O zu7`pMc{6!^7U}XJ<#Z}tjn~rbkfC=);r6Yf?}cW_~C8DTE`TK=RUFDoUBir5#u;d|N&20+RC4$?NJ7EMF-Re7gA z(1y8(88l0;>dp1nGlk#=_s6A3EX{^)IT*5sP=n|?t^EBZqSB)Q==!&hWTjiueQm!~ zG^+}JUxc`?k6ywrB{cYNp|+VmbeDv>L6P^8Hr+6+IAjX^4#h+q$w$RRC5@C7)FqN* z7Y)|&R^km?e{D4M|GchjdU`z7m&^Kmo1SM8L5^?kWG@l5=}?Z!RK!#ll-!e1tZTO(HE0}qk8B!|#Z<7tPKf1@qpbo71~CGgnN zUd2ZJ(CB_dp*Wx2q5>;;0n{Km*XrUevk)Rb?K5L92!#IM?SAlFEZ9Pll^LHJmG(8n7@8-A~?^DB*@oy{i(oG-sju*Qk@*C zk#v-!$N_)`h$V~&N8sW3iB@sQ0Gl|ZCDWt$UWA~1UiBI0@hIpGR03iK6sVx{k+fmE z6t9hds)Y*5vXi+9W1k*hl484}@aRc^DJ<^-*E6Dabq*mBwo#spvwA;(H@M`9TFTD# zd79F}+OjceoMLDz>TUyr6vHs$#evfMb75_7KA~kW!82>cUG>A&TTJM>e=jHS8;Y9| zM((ocgwBEWq|FR>&KUZb{ zz^Cp3d}`s}_|*84R?&aTu*rH}`kn^g{oKBbp_ZtZpgb*KAO5A=C^~nge^GD*lwHfE z_Uy;^=;}68^IPoq3r~y1?+MQ86h<+Ji;hlt#pb^MHmkvLDzOaWwO31fWvJ3Yy0|dN zPF4g)3APH@KrfgGHr9tEU=h$AD#r&qiY>IB*1&A@B*q8uHN4QTc^`=tS>FIthTPI| zhmLemd8UEz33@EWhj+9M@^733u~y9hb5AOau{!ykDFcQF+aR?1seiJa4_-m-X8R&Y z1y@2Cx#(d-QRXWWw|+Sb^hqLPLl4w=i@7s6S~}ZcKl27?>i0-9y+)XUMo~JSct!_D z1RfP*;4%}<3$SAjax5~fC-t5$UxS`?FSAJ9&8oW6IL z9UbfZHF_Uwka2iFs973G45FD}mRgV}8i@Vtm^;k?*e+y6+>R9(nvp@%gw;i@Kyi#btgVloYM zP}q97sPynh4df-;vUE@jx{9!~uy#{{JF_zB2G*^4bqT$cIjp+oaFz3GyTXXrxyJF)B(&qgEVVOFzswiSqq8w~ff0huj zBxU2GNvH33^}f!>VG`A_qt2U{A{Kyq6tj>WN?3r9XARM{E0DwP`l6VX}OWQPwn@54rJVOeJqD9exd;y6H1GtmGqY`w%U7WVxQQ+f%_FBimU z>)-SK;h1cdwn_a1&@LtG65?BiOIWD`>(sDFKU)hRv(0D`LLoOK8heB8jq%lQz*%3S z7^)k9(RrGdjuB6&+TdqfMyN(jQxpD8t~)lslU|SMc9D*rX-ree=Tcx-BNqDHMd|l! z?p_4V_ZRxVGhsg%pNJ{M+CkP*YU zGfm9lG>=A)zS>qdlNrp6`9%O#23m!w;yfMj*3n}s#*>@g)pzl;0FkV}3rDtWnQrd@ zF22h}bV-WECBH&teM(32$i9Y6VjUQQbUu5@4kbYCg4JO3ei0iuwzX3t6ny=O;&ijd zn)jL~)wD2^DDTMAC~E-_LWTi}kC82xhgH3%NTg>HGgtUaB=au<2MKqdcNYt>$(ndt zb8p_DYx!BqfhmljYE?cpN&(gqYZf>JOq-5cu33-kdjm`52b~<3uqXJ?aC(=F?=M(M z>23NiuTA0$``w)nIOgtE*viK8kJJp@leQ7&l;G(S2)=rM5PGwP%#p%TEtJeVXEH}c z2TgU(3=~x!y6b_m z=*Zp3oolQ$8Fe;>RLVmwdmvu$L&7q;1Z6trbGSpJo^Fb^8@5Ss?wrO^&AU4x&xUmbPv!NZKuavyr*jNI{CEVQ`UD@_j+`douKu%g>vK_k}o3@Xt z&_wmD?vP4&9IG6Ge(1k}6gNB~CLSXJ4)y4hN4{JU)j&(M6FpX{K}Dh|CzK!GQqL;q zllq*ZL;C=BB0*yU?6md+6@ZS!I8dp?+x&s~VYF{b+JHI<%P(T}IVWl6IW|W5-gVqw z))>N(0-Y!Q9?CzoRRC{<`Yp^v;MOh#k2S%R108&z-_pr#+GM-CW9MH2o1L{I$M3V6 zL8kQ|TK?(QHW_I{o6W5?{j6nddIVS-6N&A9& z`O)S;l|HxeEiwMV+;A?jgW}Tuprz;#es9Ja6>@oo*`FLAwcN^nF@Z2>$i&w> zm0AeC(n#Dg<|;rZ6zrvZ9Zt@6OQPzSOe|ojUqjVqh9SC2rg`flZ3=SU6l>f9ZK zL9E^-4q4_gtdOPv+)arc`$R4qEY>%=hw$G-M8YbzH#<1GIl|v2^D#IY=U)=Clk))u z2_^5>Z-(gSEF4xEw0U6sqx>j<)t2+PkH0{DQbdt;JZSb?uBSOPFm7&iPyd4=d)1x8y>Ii4Lp0fp zwR<<8vs75aA7B4hQ=jtX^13dKtKFt;VmiXOS|?W$vhDPGAu)uttpAZ$GZRO%K#gFA z)+MNBKs0S{wrGeNe6crdn|mh=g_#*L6fpX#m+T;uN=%f*+!;sa9TpQoXWwoZpv4=k zYpjsIkz$#*|IP9!)AF@rsjX@LoZ|$c>jn#SuG)`eF8G{D;0r(LJO(n016w_pXrhu| zc`G~W^SrLZ&!u>D#gbVR`N4kW^~L_4bIaXyxhl ze!`Cn3M+!#kc(9LPgo{>#%Ggg#OdKucW-I)w2;H%a_<>$xj9&O3S`X>zQ=W|*(4BJ ziA-KR{&q<$t}sB1y~xVu;n;gIO^&gDK?okQ;_>0;i((@S7U~>OSa?c$Tc+Wc7{zNI z>KmP;V?o&@W%!VrT2Vo!c{pJ(Oj~NQV0&{x99N;R$+qYCd0Z9#?=O$Sm^siNL_qbT zY_fqlkN9>s{;wAFk1L~71e~}%&hfIx+=$V=vPfM?M-TnQ<>G^q>B~j1j$eVR_1Ig4Whv~{{cfEDQ@T|PEtMoJLLwW) zANwOC>{DZKGzl(Xa^uKSj5+&zboZU>p#3ByEzSbv|3yrl$j)=8mL_==(1!=B8kPsX8fFr4pvr@2UK6(0&uPq>M%1S<(7MNA|QWf;R!J!Kn)KXeL}c z1BJVZo3=b<`3KnK(dU~#)oSrh$Mul&WB!VBXi#&9{lo_+d(PPEUzZVRVv&;cmV)q( z`hMR|MdUEmaW?hRGW9!6Bd(7gYJ3-A9P?FjJN~MW;6Hf3=CeQ$Yn+9;tR4MDF(`k3 zP}E7-f#yySEYBkzGfx)vG<8TGdK=Ke0jL-}25laN~OD{=2s@D-XExc zUz1pf;E*Iv=U6t$>2|!@r1Y2q9W&YOrY3icip@1OP9!TMXKXU{7gSj>OSo}+&mBxkn z%L_m)bNMNNs|p8Ns;1K#ZQB4^nSuHKP_DiOrjsqVveoKJPxZ3>} zXg|SCoBbdf4Ii*lG6{bdrDOT1S*zexBYY>FXJdX)<2+9LOE)2Voxp&OC~KA)zlj;YZC6*n0Fm8$B&-!QV-CRO*h#a zll6Tn!t}!s*K5$d1-eIPo*&}+;Ykcj!bT7K{X(K5jD65Kua!}hE&Ncbe2BW@+pGO- zY7k8mS7J6PPCazy}MRl!f?9S%_x%jFaygW|EDjtlhm4@>Zpd0 zrI1LOFpZR^P>^iFYFik4AipF3U*g`koI7Z`yMOziRm9v|aV-1bcmB6IuH5NQz3vM_ z@!ZLCMuX3enG8BL*iZem;S>;0J{rA{Q8#Gx^F$4`3ls>*Cz()zFoCq3RMj1Wf%mZq zSDfyg$6fvQ@dc(&kpp?KvsLIOdqP-e{`Y!`Q^moO(%5zoc;YU0eB!kfUKD#1D!M(n zz4VQ)r8pNNI{Ge`=!NQOmNtj4q&4rgTMr>>k65K@k7ji@hVL4d5n9_a_RL=?lC*j_ zHt*Y6FaGzZFT6RIKy{Q*6fs%%`Xq|C{~4p<2ll506zF{Yet2IpO4PlHdw^LrhlKA! z;2orti1a{v)Dt_c%&ttqAId4JgzE%0$$Oj!|M+Si*I(Y=@Fm$z`?*vZvyp!Cl-3yx zt6DKvSY*cH(lDnbJta-EyV0m#MlodijwW@uTDbWvCf5*X4XXpdwQ)pUng!W;M7iHC zC>U|1{6V|<#tl!jU{I=hnmUugGv8K-Z>N^ghiNw#7pidH@#mkN7T&+kfQ1V%dQVeL z%3;&oVKD%eu@wYz@-1vcvlndqa$Lvx>u(Ez!>;4I*>#m$MUy^<>D>TObSxW>(J>Z) zQ}X1z`MfrjtjS@c95L8PDXiOU z>CtA#{Qmm)&zM<+$=A~L2VMLOfC$TPzr*`bYW2@<-HF{l z3$N!3SNwuG z&w_@N1I2JEzVJPF%OG5r$ge@Wbf+71i+j@%s@FyL9%Fr;u@dmjJ&oi8&1wK&Z;5S= zNY?<2;rG@yR^Kw4)km*mZ1QBc6yVos*i=}(8^VVxCGNr4SU4<#hp7Qc>DwTlmII%P zmvugjg7fxw1N&yuE!*LSlbu3t_syBo0d0fy%p{K`c%|@=*Ke2Cx%>cLUTZL`%sX@N&xx%yh6dj?_ioSPKLYAO=OnsTQ1c_c9IT;(7eYy&tRPD;;J48`TiEh8Y zo(}NbLVFw9zu$|v$*m%+*YIxy{PZ?;JA)Ghyfnsc-vezCA;<6|eLC)|J3G)ETf8jJ zb?v!*J(AP88UT757bqkf(X9B;-(Muk5iDAa%;(~1Vx6vzZC+x%G|Ji48>0;OD_%@n zf)P*as#S#q`%H0X>V!Eg%tn-#Q*r>zai4A_LU>aSQ@v+;JUUcA_>={5nd0SV;Ip5M zg~KvwT*o;>Nt(Ym04k#I0=Z$ND40gc*cmk7SgSVz&!;Dx2%tT^;XKd_E@CG~^g#?? z%u>DNl_JHC?nD^KN3yQI>eF=;4(x-aN4n`W%mEncN){BP=!0lS(>R7S->D3zB;0JWn$!`yVpg~OG^xkzkD8l%) zSIoVNxCnoD4JBmOUo`pdC`o4yDGbU!y^tznP=-a3p+FBG^l4bJ6VEN&EriinEm{T z_3#}RX|!3@F|?N^%fedoes#W^bb8sb<-9~Jdg8*$N^JhoaT@OjeY@T1f&ePp%J)iJ zFcX<^1NRkW1!?ZB8wWol;$|0@AWCsP z*6VqoMSIa^vR=u9K-)B}j2CMZlDSIXRAg#58FHjL4CSaFe%{O}6RdXOd52KNovko- z)CP5s?hpFP4K&pj+j`Jf0UnYS z=@=U6cELEoxR;iFNlt{(Tq1jBzhk;>m_DjN`)`q`vQUvJ;g-x}p1}aqP@of~@fcZz zC84)NZq>7mVHt^J3nQDdfz1QALr#w1c#PP=e#od(wQ+pg|W-;Hthx}Il804)px%9uJ^B9xmzfk8AoJNi+D4ONZWO2$TP$#l37yZ1(@ zi4qnytKORm^)!57^7Hdn5m@_ff`B~RvPlw#31y-aS_yBgWZbfK4`r`#k=fwPq};6_ zKX07Y#8zi|I4^cOvez}8bn>ZXjh@5nV2-arFSfPYZhg+?Ydk#V7_c>?Y%&+Wj(-VF z0zgQSQ@^_GY=wIZ-+dBSaA?ofkmyfHbWY$Moord#dVPK{w5hujqf@VCTsrZOrI!pL zhtml0CWo7Wc-nowhW#%I z_6KMI0>OopBMD~Qe~lwGTH^?#_hh_}nasW~-^}h7@^Is{u%n#x;uN!*%Dt6~U(=H7 zxbc{i120~i#w%TQ#obt&SIS~PM$98A)>xVM%iVy*bjiEV9hHWT^RFgPv^<7ype2U2 zl%`x<0!JWs3JP91473j_JBz&{#o@z+4^%WhfE&-kk9C5z+i6GXBx=fHiwW=?f$fv%bif$GbjWX?A+v$5xN z^z8)KZ7()0k(!{tw6QmMk}UA!tR!x(Sa|)|hF4#+LrMCp+-_{RGF_qU)Hmvh(&kPActIb9;we^8!k*w_`tAV`SmzD3 z8wa{hTur<*@cJ!OV*80sC~9~rIF%4xpgWtBX`FWt$oUrBaE@MwRv2%Il-Qwto6vA7 z1V5R~v{fPhzw~kOSJnemPT;%Gjy#L@JMBi51S3flWOrpwrIB(;xNOc#o2kn4w|FZ+ zw#$VcO#9Q3JBHJV)Uz=zUls?PZh;f3iqAisv5{akO+KpcNEq_9_gt;p-_`kje`}K;pc?hHeRWsv=D24!XN{F zHPpQZTXrh%)aJ1u)39b#^M|m3(kycpZy+=3Yq=CaI(>%D5hz>46zGaQCMH<&hZRo0 z>(m!e0--S?!npobzDhs(Hm<|l%H!Opq@kfY>~&;1qtUWRer=;Oq|d9W=R_bM+bz@a z)6c(q5DpY6NU!+PTjh^@nUgE|pBZ}|az4DO`2=bTqb5h+j!sRYU<VBC1?%N8?|K@+MAi)7%St!y5bUGEdjNmv;yKT+Lwhgr9I zpn~0`feFbpXQfn#n)1l8Oa8|;`~z+kKP1Y+I2QxO^3c+>;em#z$E>VD#Pp+nG)ZKu zJQQ=Ke;Ss@zf;^?we%R{5422Y=pBKms?R~4t{uI^^Sj=Ig(rK8Sx534`k*g<&v7~X z+<8-pT4ZIe#L(1~{L_ei{Wy=QX(C7A|H`u#SaFE6opWQ2qc7fh#VODiWU3klP>*4X zT-q($90~)Y0F^B1rnroN8(I1+zQ~|(l+nlu_y6qjzkXP_0o1v5l})$D5Hot7xB@ZesEkoP-s)ilbGC{@RSH+5{D zSSg6)t81tfn~vm2a?^hyL942M6MEbiMseW1vhmz>2!=1ZOSOW>nYvGcoS$ka2OADp z>?x`%`h`~q?De-_Xr+Zg*^YW4=@U8OOJ&mCCYr z_eoRddt1mb#}&TCsGf_f(sSd++;Fgl2|ktb?yL8dr8$yHi=pmkVZnUu7cs2(j8V;*JV;%+hkbX(b zLfB*Rp^>x&%Y4&469Lr4>AuNQ4aK)uF)@`-{M-%9?Hh$8(RB{3h|Tb-;j<*?ug}G+ zT*{3l=qHuil1?stvMsOL*P*IHo%_lf?ieFfhTG?abKxyhg7L|nWDnBr3lg<+n=NkE z8&9Br!6$A}S6G2cY|+Nf(;%MLM!y<#l^7t}A#c693+A%1^TKIZdJLaVRJdVrBi^b& zkG^a^vlWO460x~H+aS+`uRRZ&p#FJQ z<9wo&dF6kOJF1=(G*wG;6t~{Sw$JT$C%)?+FJS=YFn0OH#B_vM5>{#D)Oh|<8VLX9 zDY3+L1M2|%S=YYhmlP_f>bUcqX@_pBvNxN;0m|w^iKUi;gLkYhq~rAGK4)#@=WSlV z-796<3Awkm{->^;RSE|S4;)~afhNC_)+h&7YpcIjkyX5x7Ap zV`rm5OBFL$^W%t2{3#u9gjk$d^KsS41~`kT4KR2ZDgWxRoUWy9A zdw;LO_po9c*k>>kO(vkL8bE~n4r6Hx#o#X7&MUPgE-4X+>K;=HI6Sqvd~J`$ZC+^d z9RL9Gw@*(dPdlcxKGyuGw$k_7XePl)UcM>llx4Wp?>qM(S$@r9sAlfoko+Ip120Cr z->uJ^^>U4u`r4CF`!J%se~r8W*=kDEBdNA+dsm*MwCy+KR^}?IEZ#=izJHKuwHCO9 zsLQ2T_-YZL4juELsc^(#EgD=|M}pICJ(h`}MaD&dDdw>~@ylFhnFx-zzMxepu?xN% z+bUFo&1?F)Gh4HFa`Lkw^X7voIs=UY0n_$qY?#u#KIl;lyJUqeMJTvUK-0cu%Uttv zB@ZywbplBdJVS;L>JFB2<#{N>p+&3ZV;R_X*aE1wx`kQ;J;OByLnF1ufDc1Sk2I4? zvZ&FVmrZA~+f=YM71vX_DHiI-RUn23GMvQ*(1$C}u!-T-oBkzVPumWT+tH zv2w^~b6lHsp+|;T{G>LDMQEt8`@$s1EvFJ{*odIs(0^PCo_~I_g51PQ-SkhbY$>^a z)`ha-N7I}-p|IIlAyhGjq+5(S8j`#;3#JFB)Xv|PCZfXQLT9=;81R63H$&=*_L7O; zPy289ch>!{d(k89(KnTGHUUl`59}J&HdXH4jz!jB6*{UFlRlngmMhgKK#bU$D{&2G zmP!0QuxG!^wLXe>!U6OV()igk&ViZbPR)ic~%^=rPThl>C*l^*k!&i~Gw8>ua0Dy2v-)E+@i9g5e8mt+A{tgm?4ses;2l%vi zu8Z~#%ESA|-@^UCL$YHPdlo<^pqMHHZ-*vKK3KYyq_+m=`x;l(xE+AU&ZN5fhpPb? z6BdUkEN7dQ*|QpDBS+nM{sjUosT5Fng=o-uQzAq`A=dn7nd3%snV#u`U++DvXN-AV z;2a2L`#P*AJQoKmHt*faxEw=w^zD)WEnW8<*xAP>WcSCDLKc|E>JNV;oB;0}MwdVf zfLp5G>~jeB6W#mvg6c$)K;X{tUW=HD@S(h<+U-b?Z)Jt>r%sx z&+#um9>(lRe#APoJof4mn16aP=Y`tK9bls}K;2QmDnY3p+GA={z3L8<^jd{B1d8^= z#U}yO8OSqG9QJU9nxRXNuV!I|@pQMM;mb9`pW`1o=~Rc?=Bs7_;Pg|k|97dJ`;ii1 zbty=QyJf=AU@~KaY9`yz{PQv{z3%_0JEoe`vbkETWp4pwgL&-p=uED%cwe^WH-nd{ z?d;ubsd90E*#b4nE$w=KZ8qqiw;mbsc^3r(9$WHRX}JyU5pczb;^_qI2g_R)jkV&v zpX(H3?P@}GVUCdD_(`&x#h5*; z=Li;}tzxZEXV_k*tYBwYxS<$kJNwK`TjIJ<%|~iyn-}=Jt@XOE`K360af6&`fnf2- zF(>yogd3YPr!#Ls#N>;;L@uFutO9u(+q3xBweZ$IFDjJs~ zeltz{Q!friP$tCapJK;A$f$Q7yPpT#{CjC%MEXJASoYobKctGYQ$m{D1;QaQ*QsD% zv|9ckr5YvZ8x3qmkT5ZFM;$!c}8v+)u>#eMq1 zh>4E^JV}t*Kq;%jI+v*ns@yL4M4z!MC9%(QJAziEdc%vOJ0^BH_yZRi^q2&q(wS&# z_L}6!<<8BUPXRG47IB4yk-M%e-k?eE(mYe3rGSpeiQC6ALxAFO&C>dR=?*gpSf-p8 zuS74;@j8n{86hz3=zR2LDDLU+vM`sGVXs4nT(_}5)Zb*3{#lFM?1zs|IXRqfpVFKA zEXp8^HQC9ts0W5>7Xwvs`<~~d5I^OZAGVm`4tb_Hl<2lUgRs$#6Z*Zg@eh>uD;)h^ zrGy(j`bd#21~{2;%$RAoUnC)F4bP&i=-iY@-M3iT#bTjw;@Vnye0y|BrR9dHL~%C4 z9OfE3VNCGSa$HZFHDDJpe>P)+s@JH&UAmJ^d-F+D@P8_Tv>songr;fwTs1;wnq$oV*4&k?gfy(bbv$LE`gtD^H~2eJEEI~``46B z=ZUN=!Z`XQ@p$ORh{xN|w;4~BG**SP$FiH>N0~nns5}VTs(h?cK;3MqQphPf&?C{$ z0(y091Jalv_YGK|v{s-`rpOyw174ajxMyr&1+Cp;r(VWaK1IwwAYuhMi~9A>(Fn;e zs;QzJAy7Vo?}3Dx)9G?Sai>S!r=9at($XSjp!yPRKq4ww92Hq=I*H>rvDJ}@AkcuO z-ZKK|<^D)F)|X6qH;M87!3;#~ounIWtbv_75t&d7cM^$om=WMv^iq5lB_auA{fGYFMN6 zHI3{3Z9kaBG~702ukMu|q~eM>x@~^Sz;ZY46+YO&NRWwU!lp4O;fbY+9GhxeL$)>; zVJG~b9yB>x=3SAgT@Rfs>rIt4V;p}K7CBF;zslO3U4HyW%S0TtBEX97DNbqvUc)-LJk%%ye=D!Iq)QOuk z_+HIC;OlTmZld*vmdN|fIULekOQ4i!{ zd=aNfjzu*cB~hb1vm-i96rX+oCa&9L<8$~pD2k$NR?==OSqRGpyES!}puB7dO%6Z% zF1s^E&3g?LX}lQoA!@3P3o-D3Fj z#0OG%Yg!C1&CeVV6+#=GOBCS|&Lu3NMS((7G`FadZ<%&d>QVV#{^qVri86dpf%`e? zlx~L1%yz&O-XF$s2<@s027Dl9v*TkW!Rpod4<(|6pI6Mc4*im?p2&h$EAe}qGlPP~ zWfKA)+ZC=7V;)<_2(bs1n_+BLv+;Z0-(6fSHxA{zeGr7pwQOU*k4hZeK7Ud(jxoAl zSNH5hEI{z+Vcbuy?Tn36`z=S?ljnNJzTa2|s(?#QeK)~F<|jFvTbo%*@C^RayRb!- zovZz|Dn4(x3&+^E?3|zH5`T(CJb&&O)&8e+k^G|eW!3yxsl56r?&Gxm= zR%RB0L>Fba&GM*V+_7SKnm1vE=h&FAHUYAn?L+^YHUaHp5%x_}ov{CtHUTzcN#yT- zRn*5d9uY4vjN~h8?3M!w0=TYCY zw&q64ov<&jgysuHKt~Jag%bA&MCt{D*7E)fk5t|Dixt;Uis!bduZOtUb{}4BQ9O*v zeE45~fUiDz61Wz55-5&i!C|4~TSxE_0pP&J4)e8q+Ax8ffg=3=n`S!>3on;Ki!!nu?lT)h z`h*XP+6wrOY$k{ErH8Bkt8wgK60{Im6y52AZ@##mu<|Lmp|nkB%!*t~1*x`G3}li? zm2QukJILlyj4u2A9cMY49AA;^koIPx${mWUr+xq_>`x2HHN@|wURJc4>dv>#EN6dM z?k0q?eBS49Yjg1>zywz|)N22&^1koSd$*CwGgCF$_*k*tH(nrhX+ocPVaQkf?)0;n z`DcfXc<=R%&7P<1q*1*PxNeHLsL6Wph!V9T-=rGuH5@%w6!U@iVl(P$OepBH#Hd3hLUSpl|2?d zai^Si)~_E~s}`jK1=Af-%ZKQryJ?xa!MYAS;X6f$EnAL|+j7cyJ@W5z#aDtMGCp%* z7Q-=xRURXxp%cGMlcnGT;Z0}?O+C86rm}k1fcS$+9;3(}qN(Hk!Q-gdC^FU^Is_|r z0OzHo&)lAtWLXU4O>V7>$&W-{YH5_NA*W#QL1i4p3lq-X0@SbHoC`#4!l9EOe{7tn zwUgP!?hPZbM(GHjCVqj-;u5Tfo@o-8jwMvQ)V7hvto_b0RP-ulp`&&sZs+};jmi;RY!WfqK3G@$4O}uan}7w;&>Fpd(W-o}@z;J9ymwb@=Omu1K(8QE&WJ z=XaFJn~PGM2$Vi`$DsCc*xgI%L?(tTYz^~S++JnUj~dP&Z+@hp5{1r{!KnqfcGl=z z7w60RR%!thz-j~f;E^JLSbcF}P3r~HG4j55-unNb4g3R@AD5B7O0(fLWVI^TyzwR3=kwdtetA7ZJ=!T6n7>}HQXO9pxd{zN_)a`-KwfUTHAzs zrx0o^x_c7K=^T!Y1^3?jOs9e^UG-f?Ofi5hj8Or#NOezDu{U;3hCwE*)mi>%Nv9#+ z2iS3D9%RLK{e+z**dAp-YxboA@V?>>b?r-jg-CT;Hm^$IH9D)2Z_U zN1D*d&Zw8soS$XtVgn12=jYEP7{$UaImy@Cs>^x&+(xH8M+3-0={laMUME3%04&b+ z1#C9EkH<83Q~TaDc;~>fJgc9c#r6WmNg6pEqY5ad!5+gCiU^yJwi-*;cqwZYt6U;i z^^rKehYF-ZTTA4oOQ=a873#wpVZVP(8_%c|7kTj-MdU7M&PuU*G9c-wYZ(_106LB4 zZ9q#4hyWZ`V7nyw*c#xoqltYh|C&4Zzo76f5VHHJQ7inAhvy2=1=45e8h$Deu z%z0$H?bMxBIl1@l^^4j#&az_4U=sI1V8PwRm2UbpHX~@uH8%nCWiiF00$m_S=7II*aC~qJ}Ge_|eG?T~AjG zvMiwGNQ&rUr>8yRw1QG4!p9D&mq}_4HZed@nCnQ!U zH%+eh^vyOFpanBHHWs05<*ijf6Ssx|!Yl-q&BqF`($i!0nXw9v-?eC`$CIbXaM6PW z>?~^r2%cvToyP*dA^@;i1Ssa0eJm6+3S>x_)Ckdd+sf~+yi{DgM)c}{QzQl=w%_J7 zk95t50EV&dviksQk{cgX>H-&W9~ek3p)f<1Vufah&$<%d{Q!87_0~V9EE^48i#IHp zoYd69q^c{4kwX*5mi6lhMkUouV*&QnqPC#Yk4vAoY*%Y|b-BG~B|9=sN}@l-{H%)) z7dj23_8qqENIy(+uOmi>ro9P$o@%D>TKm(^2&V>yL#)mri4N7`u-I1vNfR^V(s!L9dS`R5TZG?Q|xK1JeK!BnC7ShQoNQ>!+{VzZBR|0N2|zN z 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 literal 0 HcmV?d00001