Refactor Game Monitor into Game Manager with Setup.py integration and full process control

- Replaced legacy `game_monitor.py` with a new modular `game_manager.py`.
- Introduced `GameMonitor` class to encapsulate:
  - Game window detection, focus enforcement, and resize enforcement.
  - Timed game restarts based on configuration interval.
  - Callback system to notify Setup.py on restart completion.
  - Cross-platform game launching (Windows/Unix).
  - Process termination using `psutil` if available.

- `Setup.py` now acts as the control hub:
  - Instantiates and manages `GameMonitor`.
  - Provides live configuration updates (e.g., window title, restart timing).
  - Coordinates bot lifecycle with game restarts.

- Maintains standalone execution mode for `game_manager.py` (for testing or CLI use).
- Replaces older “always-on-top” logic with foreground window activation.
- Dramatically improves control, flexibility, and automation reliability for game-based workflows.
This commit is contained in:
z060142 2025-05-13 03:40:14 +08:00
parent a5b6a44164
commit 51a99ee5ad
5 changed files with 708 additions and 515 deletions

View File

@ -15,72 +15,66 @@ Wolf Chat 是一個基於 MCP (Modular Capability Provider) 框架的聊天機
### 核心元件
1. **主控模塊 (main.py)**
- 協調各模塊的工作
- 初始化 MCP 連接
- **容錯處理**:即使 `config.py` 中未配置 MCP 伺服器或所有伺服器連接失敗程式現在也會繼續執行僅打印警告訊息MCP 功能將不可用。 (Added 2025-04-21)
- **伺服器子進程管理 (修正 2025-05-02)**:使用 `mcp.client.stdio.stdio_client` 啟動和連接 `config.py` 中定義的每個 MCP 伺服器。`stdio_client` 作為一個異步上下文管理器,負責管理其啟動的子進程的生命週期。
- **Windows 特定處理 (修正 2025-05-02)**:在 Windows 上,如果 `pywin32` 可用,會註冊一個控制台事件處理程序 (`win32api.SetConsoleCtrlHandler`)。此處理程序主要用於輔助觸發正常的關閉流程(最終會調用 `AsyncExitStack.aclose()`),而不是直接終止進程。伺服器子進程的實際終止依賴於 `stdio_client` 上下文管理器在 `AsyncExitStack.aclose()` 期間的清理操作。
- **記憶體系統初始化 (新增 2025-05-02)**:在啟動時調用 `chroma_client.initialize_memory_system()`,根據 `config.py` 中的 `ENABLE_PRELOAD_PROFILES` 設定決定是否啟用記憶體預載入。
- 設置並管理主要事件循環
- **記憶體預載入 (新增 2025-05-02)**:在主事件循環中,如果預載入已啟用,則在每次收到 UI 觸發後、調用 LLM 之前,嘗試從 ChromaDB 預先獲取用戶資料 (`get_entity_profile`)、相關記憶 (`get_related_memories`) 和潛在相關的機器人知識 (`get_bot_knowledge`)。
- 處理程式生命週期管理和資源清理(通過 `AsyncExitStack` 間接管理 MCP 伺服器子進程的終止)
1. **主控模塊 (main.py)**
- 協調各模塊的工作
- 初始化 MCP 連接
- **容錯處理**:即使 `config.py` 中未配置 MCP 伺服器或所有伺服器連接失敗程式現在也會繼續執行僅打印警告訊息MCP 功能將不可用。 (Added 2025-04-21)
- **伺服器子進程管理 (修正 2025-05-02)**:使用 `mcp.client.stdio.stdio_client` 啟動和連接 `config.py` 中定義的每個 MCP 伺服器。`stdio_client` 作為一個異步上下文管理器,負責管理其啟動的子進程的生命週期。
- **Windows 特定處理 (修正 2025-05-02)**:在 Windows 上,如果 `pywin32` 可用,會註冊一個控制台事件處理程序 (`win32api.SetConsoleCtrlHandler`)。此處理程序主要用於輔助觸發正常的關閉流程(最終會調用 `AsyncExitStack.aclose()`),而不是直接終止進程。伺服器子進程的實際終止依賴於 `stdio_client` 上下文管理器在 `AsyncExitStack.aclose()` 期間的清理操作。
- **記憶體系統初始化 (新增 2025-05-02)**:在啟動時調用 `chroma_client.initialize_memory_system()`,根據 `config.py` 中的 `ENABLE_PRELOAD_PROFILES` 設定決定是否啟用記憶體預載入。
- 設置並管理主要事件循環
- **記憶體預載入 (新增 2025-05-02)**:在主事件循環中,如果預載入已啟用,則在每次收到 UI 觸發後、調用 LLM 之前,嘗試從 ChromaDB 預先獲取用戶資料 (`get_entity_profile`)、相關記憶 (`get_related_memories`) 和潛在相關的機器人知識 (`get_bot_knowledge`)。
- 處理程式生命週期管理和資源清理(通過 `AsyncExitStack` 間接管理 MCP 伺服器子進程的終止)
2. **LLM 交互模塊 (llm_interaction.py)**
- 與語言模型 API 通信
- 管理系統提示與角色設定
- **條件式提示 (新增 2025-05-02)**`get_system_prompt` 函數現在接受預載入的用戶資料、相關記憶和機器人知識。根據是否有預載入數據,動態調整系統提示中的記憶體檢索協議說明。
- 處理語言模型的工具調用功能
- 格式化 LLM 回應
- 提供工具結果合成機制
2. **LLM 交互模塊 (llm_interaction.py)**
- 與語言模型 API 通信
- 管理系統提示與角色設定
- **條件式提示 (新增 2025-05-02)**`get_system_prompt` 函數現在接受預載入的用戶資料、相關記憶和機器人知識。根據是否有預載入數據,動態調整系統提示中的記憶體檢索協議說明。
- 處理語言模型的工具調用功能
- 格式化 LLM 回應
- 提供工具結果合成機制
3. **UI 互動模塊 (ui_interaction.py)**
- 使用圖像辨識技術監控遊戲聊天視窗
- 檢測聊天泡泡與關鍵字
- 複製聊天內容和獲取發送者姓名
- 將生成的回應輸入到遊戲中
3. **UI 互動模塊 (ui_interaction.py)**
- 使用圖像辨識技術監控遊戲聊天視窗
- 檢測聊天泡泡與關鍵字
- 複製聊天內容和獲取發送者姓名
- 將生成的回應輸入到遊戲中
4. **MCP 客戶端模塊 (mcp_client.py)**
- 管理與 MCP 服務器的通信
- 列出和調用可用工具
- 處理工具調用的結果和錯誤
4. **MCP 客戶端模塊 (mcp_client.py)**
- 管理與 MCP 服務器的通信
- 列出和調用可用工具
- 處理工具調用的結果和錯誤
5. **配置模塊 (config.py)**
- 集中管理系統參數和設定
- 整合環境變數
- 配置 API 密鑰和服務器設定
5. **配置模塊 (config.py)**
- 集中管理系統參數和設定
- 整合環境變數
- 配置 API 密鑰和服務器設定
6. **角色定義 (persona.json)**
- 詳細定義機器人的人格特徵
- 包含外觀、說話風格、個性特點等資訊
- 提供給 LLM 以確保角色扮演一致性
6. **角色定義 (persona.json)**
- 詳細定義機器人的人格特徵
- 包含外觀、說話風格、個性特點等資訊
- 提供給 LLM 以確保角色扮演一致性
7. **遊戲視窗監控模組 (game_monitor.py)** (取代 window-setup-script.py 和舊的 window-monitor-script.py)
- 持續監控遊戲視窗 (`config.WINDOW_TITLE`)。
- 確保視窗維持在設定檔 (`config.py`) 中指定的位置 (`GAME_WINDOW_X`, `GAME_WINDOW_Y`) 和大小 (`GAME_WINDOW_WIDTH`, `GAME_WINDOW_HEIGHT`)。
- **確保視窗保持活躍**:如果遊戲視窗不是目前的前景視窗,則嘗試將其帶到前景並啟用 (Bring to Foreground/Activate),取代了之前的強制置頂 (Always on Top) 邏輯 (修改於 2025-05-12)。
- **定時遊戲重啟** (如果 `config.ENABLE_SCHEDULED_RESTART` 為 True)
- 根據 `config.RESTART_INTERVAL_MINUTES` 設定的間隔執行。
- **簡化流程 (2025-04-25)**
1. 通過 `stdout``main.py` 發送 JSON 訊號 (`{'action': 'pause_ui'}`),請求暫停 UI 監控。
2. 等待固定時間30 秒)。
3. 調用 `restart_game_process` 函數,**嘗試**終止 (`terminate`/`kill`) `LastWar.exe` 進程(**無驗證**)。
4. 等待固定時間2 秒)。
5. **嘗試**使用 `os.startfile` 啟動 `config.GAME_EXECUTABLE_PATH`**無驗證**)。
6. 等待固定時間30 秒)。
7. 使用 `try...finally` 結構確保**總是**執行下一步。
8. 通過 `stdout``main.py` 發送 JSON 訊號 (`{'action': 'resume_ui'}`),請求恢復 UI 監控。
- **視窗調整**:遊戲視窗的位置/大小/置頂狀態的調整完全由 `monitor_game_window` 的主循環持續負責,重啟流程不再進行立即調整。
- **作為獨立進程運行**:由 `main.py` 使用 `subprocess.Popen` 啟動,捕獲其 `stdout` (用於 JSON 訊號) 和 `stderr` (用於日誌)。
- **進程間通信**
- `game_monitor.py` -> `main.py`:通過 `stdout` 發送 JSON 格式的 `pause_ui``resume_ui` 訊號。
- **日誌處理**`game_monitor.py` 的日誌被配置為輸出到 `stderr`,以保持 `stdout` 清潔,確保訊號傳遞可靠性。`main.py` 會讀取 `stderr` 並可能顯示這些日誌。
- **生命週期管理**:由 `main.py` 在啟動時創建,並在 `shutdown` 過程中嘗試終止 (`terminate`)。
7. **遊戲管理器模組 (game_manager.py)** (取代舊的 `game_monitor.py`)
- **核心類 `GameMonitor`**:封裝所有遊戲視窗監控、自動重啟和進程管理功能。
- **由 `Setup.py` 管理**
- 在 `Setup.py` 的 "Start Managed Bot & Game" 流程中被實例化和啟動。
- 在停止會話時由 `Setup.py` 停止。
- 設定(如視窗標題、路徑、重啟間隔等)通過 `Setup.py` 傳遞,並可在運行時通過 `update_config` 方法更新。
- **功能**
- 持續監控遊戲視窗 (`config.WINDOW_TITLE`)。
- 確保視窗維持在設定檔中指定的位置和大小。
- 確保視窗保持活躍(帶到前景並獲得焦點)。
- **定時遊戲重啟**:根據設定檔中的間隔執行。
- **回調機制**:重啟完成後,通過回調函數通知 `Setup.py`(例如,`restart_complete``Setup.py` 隨後處理機器人重啟。
- **進程管理**:使用 `psutil`(如果可用)查找和終止遊戲進程。
- **跨平台啟動**:使用 `os.startfile` (Windows) 或 `subprocess.Popen` (其他平台) 啟動遊戲。
- **獨立運行模式**`game_manager.py` 仍然可以作為獨立腳本運行 (類似舊的 `game_monitor.py`),此時它會從 `config.py` 加載設定,並通過 `stdout` 發送 JSON 訊號。
8. **ChromaDB 客戶端模塊 (chroma_client.py)** (新增 2025-05-02)
- 處理與本地 ChromaDB 向量數據庫的連接和互動。
- 提供函數以初始化客戶端、獲取/創建集合,以及查詢用戶資料、相關記憶和機器人知識。
- 使用 `chromadb.PersistentClient` 連接持久化數據庫。
8. **ChromaDB 客戶端模塊 (chroma_client.py)** (新增 2025-05-02)
- 處理與本地 ChromaDB 向量數據庫的連接和互動。
- 提供函數以初始化客戶端、獲取/創建集合,以及查詢用戶資料、相關記憶和機器人知識。
- 使用 `chromadb.PersistentClient` 連接持久化數據庫。
### 資料流程
@ -638,6 +632,39 @@ Wolf Chat 是一個基於 MCP (Modular Capability Provider) 框架的聊天機
- 添加主題識別與記憶功能
- 探索多輪對話中的上下文理解能力
## 最近改進2025-05-13
### 遊戲監控模組重構
- **目的**:將遊戲監控功能從獨立的 `game_monitor.py` 腳本重構為一個更健壯、更易於管理的 `game_manager.py` 模組,並由 `Setup.py` 統一控制其生命週期和配置。
- **`game_manager.py` (新模組)**
- 創建了 `GameMonitor` 類,封裝了所有遊戲視窗監控、自動重啟和進程管理邏輯。
- 提供了 `create_game_monitor` 工廠函數。
- 支持通過構造函數和 `update_config` 方法進行配置。
- 使用回調函數 (`callback`) 與調用者(即 `Setup.py`)通信,例如在遊戲重啟完成時。
- 保留了獨立運行模式,以便在直接執行時仍能工作(主要用於測試或舊版兼容)。
- 程式碼註解和日誌訊息已更新為英文。
- **`Setup.py` (修改)**
- 導入 `game_manager`
- 在 `WolfChatSetup` 類的 `__init__` 方法中初始化 `self.game_monitor = None`
- 在 `start_managed_session` 方法中:
- 創建 `game_monitor_callback` 函數以處理來自 `GameMonitor` 的動作(特別是 `restart_complete`)。
- 使用 `game_manager.create_game_monitor` 創建 `GameMonitor` 實例。
- 啟動 `GameMonitor`
- 新增 `_handle_game_restart_complete` 方法,用於在收到 `GameMonitor` 的重啟完成回調後,處理機器人的重啟。
- 在 `stop_managed_session` 方法中,調用 `self.game_monitor.stop()` 並釋放實例。
- 修改 `_restart_game_managed` 方法,使其在 `self.game_monitor` 存在且運行時,調用 `self.game_monitor.restart_now()` 來執行遊戲重啟。
- 在 `save_settings` 方法中,如果 `self.game_monitor` 實例存在,則調用其 `update_config` 方法以更新運行時配置。
- **`main.py` (修改)**
- 移除了所有對舊 `game_monitor.py` 的導入、子進程啟動、訊號讀取和生命週期管理相關的程式碼。遊戲監控現在完全由 `Setup.py` 在受管會話模式下處理。
- **舊檔案刪除**
- 刪除了原來的 `game_monitor.py` 文件。
- **效果**
- 遊戲監控邏輯更加內聚和模塊化。
- `Setup.py` 現在完全控制遊戲監控的啟動、停止和配置,簡化了 `main.py` 的職責。
- 通過回調機制實現了更清晰的模塊間通信。
- 提高了程式碼的可維護性和可擴展性。
### 注意事項
1. **圖像模板**:確保所有必要的 UI 元素模板都已截圖並放置在 templates 目錄
@ -741,3 +768,42 @@ ClaudeCode.md
# Current Mode
ACT MODE
</environment_details>
</file_content>
Now that you have the latest state of the file, try the operation again with fewer, more precise SEARCH blocks. For large files especially, it may be prudent to try to limit yourself to <5 SEARCH/REPLACE blocks at a time, then wait for the user to respond with the result of the operation before following up with another replace_in_file call to make additional edits.
(If you run into this error 3 times in a row, you may use the write_to_file tool as a fallback.)
</error><environment_details>
# VSCode Visible Files
ClaudeCode.md
# VSCode Open Tabs
config_template.py
test/llm_debug_script.py
llm_interaction.py
wolf_control.py
.gitignore
chroma_client.py
batch_memory_record.py
memory_manager.py
game_monitor.py
game_manager.py
Setup.py
main.py
ClaudeCode.md
reembedding tool.py
config.py
memory_backup.py
tools/chroma_view.py
ui_interaction.py
remote_config.json
# Current Time
5/13/2025, 3:31:34 AM (Asia/Taipei, UTC+8:00)
# Context Window Usage
429,724 / 1,048.576K tokens used (41%)
# Current Mode
ACT MODE
</environment_details>

View File

@ -29,6 +29,7 @@ import schedule
import psutil
import random # Added for exponential backoff jitter
import urllib3 # Added for SSL warning suppression
import game_manager # Added for new game monitoring module
try:
import socketio
HAS_SOCKETIO = True
@ -604,6 +605,9 @@ class WolfChatSetup(tk.Tk):
# Initialize scheduler process tracker
self.scheduler_process = None
# Initialize game monitor instance (will be created in start_managed_session)
self.game_monitor = None
# Set initial states based on loaded data
@ -747,7 +751,39 @@ class WolfChatSetup(tk.Tk):
# Start Monitoring Thread
self._start_monitoring_thread()
self._start_monitoring_thread() # This is the old general monitoring thread
# Initialize and start GameMonitor (new specific game monitor)
try:
# Create callback function for game_monitor
def game_monitor_callback(action):
logger.info(f"Received action from game_manager: {action}")
if action == "restart_complete":
# Schedule _handle_game_restart_complete to run in the main thread
self.after(0, self._handle_game_restart_complete)
# Add other actions if needed, e.g., "restart_begin", "restart_error"
# Create GameMonitor instance if it doesn't exist
if not self.game_monitor:
self.game_monitor = game_manager.create_game_monitor(
config_data=self.config_data,
remote_data=self.remote_data,
logger=logger, # Use the main Setup logger
callback=game_monitor_callback
)
# Start the game monitor
if self.game_monitor.start(): # Ensure start() returns a boolean
logger.info("Game monitor (game_manager) started successfully.")
else:
logger.error("Failed to start game_manager's GameMonitor.")
messagebox.showwarning("Warning", "Game window monitoring (game_manager) could not be started.")
# Continue execution, not a fatal error for the whole session
except Exception as gm_err:
logger.exception(f"Error setting up game_manager's GameMonitor: {gm_err}")
messagebox.showwarning("Warning", "Failed to initialize game_manager's GameMonitor.")
# Continue execution
# Start Scheduler Thread
self._start_scheduler_thread()
@ -756,6 +792,23 @@ class WolfChatSetup(tk.Tk):
# messagebox.showinfo("Session Started", "Managed bot and game session started. Check console for logs.") # Removed popup
logger.info("Managed bot and game session started. Check console for logs.") # Log instead of popup
def _handle_game_restart_complete(self):
"""Handles the callback from GameMonitor when a game restart is complete."""
logger.info("Game restart completed (callback from game_manager). Handling bot restart...")
try:
# Ensure we are in the main thread (already handled by self.after)
# Wait a bit for the game to stabilize
time.sleep(10)
logger.info("Restarting bot after game restart (triggered by game_manager)...")
if self._restart_bot_managed():
logger.info("Bot restarted successfully after game_manager's game restart.")
else:
logger.error("Failed to restart bot after game_manager's game restart!")
messagebox.showwarning("Warning", "Failed to restart bot after game_manager's game restart.")
except Exception as e:
logger.exception(f"Error in _handle_game_restart_complete: {e}")
def stop_managed_session(self):
logger.info("Attempting to stop managed session...")
self.keep_monitoring_flag.clear() # Signal threads to stop
@ -778,7 +831,19 @@ class WolfChatSetup(tk.Tk):
if self.monitor_thread_instance.is_alive():
logger.warning("Monitor thread did not stop in time.")
self.monitor_thread_instance = None
# Stop GameMonitor (from game_manager)
if self.game_monitor:
try:
if self.game_monitor.stop(): # Ensure stop() returns a boolean
logger.info("Game monitor (game_manager) stopped successfully.")
else:
logger.warning("Game monitor (game_manager) stop may have failed.")
except Exception as gm_err:
logger.exception(f"Error stopping game_manager's GameMonitor: {gm_err}")
finally:
self.game_monitor = None # Release the instance
self._stop_bot_managed()
self._stop_game_managed()
@ -1063,9 +1128,16 @@ class WolfChatSetup(tk.Tk):
def _restart_game_managed(self):
logger.info("Restarting game (managed)...")
self._stop_game_managed()
time.sleep(2) # Give it time to fully stop
return self._start_game_managed()
# If GameMonitor (from game_manager) exists and is running, use it to restart
if self.game_monitor and self.game_monitor.running:
logger.info("Using game_manager's GameMonitor to restart game.")
return self.game_monitor.restart_now()
else:
# Fallback to the original method if game_monitor is not active
logger.info("game_manager's GameMonitor not active, using default method to restart game.")
self._stop_game_managed()
time.sleep(2) # Give it time to fully stop
return self._start_game_managed()
def _restart_bot_managed(self):
logger.info("Restarting bot (managed)...")
@ -2414,6 +2486,14 @@ class WolfChatSetup(tk.Tk):
save_env_file(self.env_data)
generate_config_file(self.config_data, self.env_data)
save_remote_config(self.remote_data) # Save remote config
# If GameMonitor (from game_manager) exists, update its configuration
if self.game_monitor:
try:
self.game_monitor.update_config(self.config_data, self.remote_data)
logger.info("Game monitor (game_manager) configuration updated.")
except Exception as gm_update_err:
logger.error(f"Failed to update game_manager's GameMonitor configuration: {gm_update_err}")
if show_success_message:
messagebox.showinfo("Success", "Settings saved successfully.\nRestart managed session for changes to take effect.")

494
game_manager.py Normal file
View File

@ -0,0 +1,494 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
Game Manager Module
Provides game window monitoring, automatic restart, and process management features.
Designed to be imported and controlled by setup.py or other management scripts.
"""
import os
import sys
import time
import json
import threading
import subprocess
import logging
import pygetwindow as gw
# Attempt to import platform-specific modules that might be needed
try:
import win32gui
import win32con
HAS_WIN32 = True
except ImportError:
HAS_WIN32 = False
print("Warning: win32gui/win32con modules not installed, some window management features may be unavailable")
try:
import psutil
HAS_PSUTIL = True
except ImportError:
HAS_PSUTIL = False
print("Warning: psutil module not installed, process management features may be unavailable")
class GameMonitor:
"""
Game window monitoring class.
Responsible for monitoring game window position, scheduled restarts, and providing window management functions.
"""
def __init__(self, config_data, remote_data=None, logger=None, callback=None):
# Use the provided logger or create a new one
self.logger = logger or logging.getLogger("GameMonitor")
if not self.logger.handlers:
handler = logging.StreamHandler()
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
handler.setFormatter(formatter)
self.logger.addHandler(handler)
self.logger.setLevel(logging.INFO)
self.config_data = config_data
self.remote_data = remote_data or {}
self.callback = callback # Callback function to notify the caller
# Read settings from configuration
self.window_title = self.config_data.get("GAME_WINDOW_CONFIG", {}).get("WINDOW_TITLE", "Last War-Survival Game")
self.enable_restart = self.config_data.get("GAME_WINDOW_CONFIG", {}).get("ENABLE_SCHEDULED_RESTART", True)
self.restart_interval = self.config_data.get("GAME_WINDOW_CONFIG", {}).get("RESTART_INTERVAL_MINUTES", 60)
self.game_path = self.config_data.get("GAME_WINDOW_CONFIG", {}).get("GAME_EXECUTABLE_PATH", "")
self.window_x = self.config_data.get("GAME_WINDOW_CONFIG", {}).get("GAME_WINDOW_X", 50)
self.window_y = self.config_data.get("GAME_WINDOW_CONFIG", {}).get("GAME_WINDOW_Y", 30)
self.window_width = self.config_data.get("GAME_WINDOW_CONFIG", {}).get("GAME_WINDOW_WIDTH", 600)
self.window_height = self.config_data.get("GAME_WINDOW_CONFIG", {}).get("GAME_WINDOW_HEIGHT", 1070)
self.monitor_interval = self.config_data.get("GAME_WINDOW_CONFIG", {}).get("MONITOR_INTERVAL_SECONDS", 5)
# Read game process name from remote_data, use default if not found
self.game_process_name = self.remote_data.get("GAME_PROCESS_NAME", "LastWar.exe")
# Internal state
self.running = False
self.next_restart_time = None
self.monitor_thread = None
self.stop_event = threading.Event()
self.logger.info(f"GameMonitor initialized. Game window: '{self.window_title}', Process: '{self.game_process_name}'")
self.logger.info(f"Position: ({self.window_x}, {self.window_y}), Size: {self.window_width}x{self.window_height}")
self.logger.info(f"Scheduled Restart: {'Enabled' if self.enable_restart else 'Disabled'}, Interval: {self.restart_interval} minutes")
def start(self):
"""Start game window monitoring"""
if self.running:
self.logger.info("Game window monitoring is already running")
return True # Return True if already running
self.logger.info("Starting game window monitoring...")
self.stop_event.clear()
# Set next restart time
if self.enable_restart and self.restart_interval > 0:
self.next_restart_time = time.time() + (self.restart_interval * 60)
self.logger.info(f"Scheduled restart enabled. First restart in {self.restart_interval} minutes")
else:
self.next_restart_time = None
self.logger.info("Scheduled restart is disabled")
# Start monitoring thread
self.monitor_thread = threading.Thread(target=self._monitor_loop, daemon=True)
self.monitor_thread.start()
self.running = True
self.logger.info("Game window monitoring started")
return True
def stop(self):
"""Stop game window monitoring"""
if not self.running:
self.logger.info("Game window monitoring is not running")
return True # Return True if already stopped
self.logger.info("Stopping game window monitoring...")
self.stop_event.set()
# Wait for monitoring thread to finish
if self.monitor_thread and self.monitor_thread.is_alive():
self.logger.info("Waiting for monitoring thread to finish...")
self.monitor_thread.join(timeout=5)
if self.monitor_thread.is_alive():
self.logger.warning("Game window monitoring thread did not stop within the timeout period")
self.running = False
self.monitor_thread = None
self.logger.info("Game window monitoring stopped")
return True
def _monitor_loop(self):
"""Main monitoring loop"""
self.logger.info("Game window monitoring loop started")
last_adjustment_message = "" # Avoid logging repetitive adjustment messages
while not self.stop_event.is_set():
try:
# Check for scheduled restart
if self.next_restart_time and time.time() >= self.next_restart_time:
self.logger.info("Scheduled restart time reached. Performing restart...")
self._perform_restart()
# Reset next restart time
self.next_restart_time = time.time() + (self.restart_interval * 60)
self.logger.info(f"Restart timer reset. Next restart in {self.restart_interval} minutes")
# Continue to next loop iteration
time.sleep(self.monitor_interval)
continue
# Find game window
window = self._find_game_window()
adjustment_made = False
current_message = ""
if window:
try:
# Use win32gui functions only on Windows
if HAS_WIN32:
# Get window handle
hwnd = window._hWnd
# 1. Check and adjust position/size
current_pos = (window.left, window.top)
current_size = (window.width, window.height)
target_pos = (self.window_x, self.window_y)
target_size = (self.window_width, self.window_height)
if current_pos != target_pos or current_size != target_size:
window.moveTo(target_pos[0], target_pos[1])
window.resizeTo(target_size[0], target_size[1])
# Verify if move and resize were successful
time.sleep(0.1)
window.activate() # Try activating to ensure changes apply
time.sleep(0.1)
new_pos = (window.left, window.top)
new_size = (window.width, window.height)
if new_pos == target_pos and new_size == target_size:
current_message += f"Adjusted game window to position ({target_pos[0]},{target_pos[1]}) size {target_size[0]}x{target_size[1]}. "
adjustment_made = True
else:
self.logger.warning(f"Attempted to adjust window pos/size, but result mismatch. Target: {target_pos}/{target_size}, Actual: {new_pos}/{new_size}")
# 2. Check and bring to foreground
current_foreground_hwnd = win32gui.GetForegroundWindow()
if current_foreground_hwnd != hwnd:
try:
# Use HWND_TOP to bring window to top, not HWND_TOPMOST
win32gui.SetWindowPos(hwnd, win32con.HWND_TOP, 0, 0, 0, 0,
win32con.SWP_NOMOVE | win32con.SWP_NOSIZE)
# Set as foreground window (gain focus)
win32gui.SetForegroundWindow(hwnd)
# Verify if window is active
time.sleep(0.1)
foreground_hwnd = win32gui.GetForegroundWindow()
if foreground_hwnd == hwnd:
current_message += "Brought game window to foreground and set focus. "
adjustment_made = True
else:
# Use fallback method
self.logger.warning("SetForegroundWindow failed, trying fallback window.activate()")
try:
window.activate()
time.sleep(0.1)
if win32gui.GetForegroundWindow() == hwnd:
current_message += "Set game window focus using fallback method. "
adjustment_made = True
except Exception as activate_err:
self.logger.warning(f"Fallback method window.activate() failed: {activate_err}")
except Exception as focus_err:
self.logger.warning(f"Error setting window focus: {focus_err}")
else:
# Use basic functions on non-Windows platforms
current_pos = (window.left, window.top)
current_size = (window.width, window.height)
target_pos = (self.window_x, self.window_y)
target_size = (self.window_width, self.window_height)
if current_pos != target_pos or current_size != target_size:
window.moveTo(target_pos[0], target_pos[1])
window.resizeTo(target_size[0], target_size[1])
current_message += f"Adjusted game window to position {target_pos} size {target_size[0]}x{target_size[1]}. "
adjustment_made = True
# Try activating the window (may have limited effect on non-Windows)
try:
window.activate()
current_message += "Attempted to activate game window. "
adjustment_made = True
except Exception as activate_err:
self.logger.warning(f"Error activating window: {activate_err}")
except Exception as e:
self.logger.error(f"Unexpected error while monitoring game window: {e}")
# Log only if adjustments were made and the message changed
if adjustment_made and current_message and current_message != last_adjustment_message:
self.logger.info(f"[GameMonitor] {current_message.strip()}")
last_adjustment_message = current_message
elif not window:
# Reset last message if window disappears
last_adjustment_message = ""
except Exception as e:
self.logger.error(f"Error in monitoring loop: {e}")
# Wait for the next check
time.sleep(self.monitor_interval)
self.logger.info("Game window monitoring loop finished")
def _find_game_window(self):
"""Find the game window with the specified title"""
try:
windows = gw.getWindowsWithTitle(self.window_title)
if windows:
return windows[0]
except Exception as e:
self.logger.debug(f"Error finding game window: {e}")
return None
def _find_game_process(self):
"""Find the game process"""
if not HAS_PSUTIL:
self.logger.warning("psutil is not available, cannot perform process lookup")
return None
try:
for proc in psutil.process_iter(['pid', 'name', 'exe']):
try:
proc_info = proc.info
proc_name = proc_info.get('name')
if proc_name and proc_name.lower() == self.game_process_name.lower():
self.logger.info(f"Found game process '{proc_name}' (PID: {proc.pid})")
return proc
except (psutil.NoSuchProcess, psutil.AccessDenied, psutil.ZombieProcess):
continue
except Exception as e:
self.logger.error(f"Error finding game process: {e}")
self.logger.info(f"Game process '{self.game_process_name}' not found")
return None
def _perform_restart(self):
"""Execute the game restart process"""
self.logger.info("Starting game restart process")
try:
# 1. Notify that restart has begun (optional)
if self.callback:
self.callback("restart_begin")
# 2. Terminate existing game process
self._terminate_game_process()
time.sleep(2) # Short wait to ensure process termination
# 3. Start new game process
if self._start_game_process():
self.logger.info("Game restarted successfully")
else:
self.logger.error("Failed to start game")
# 4. Wait for game to launch
restart_wait_time = 30 # seconds
self.logger.info(f"Waiting for game to start ({restart_wait_time} seconds)...")
time.sleep(restart_wait_time)
# 5. Notify restart completion
self.logger.info("Game restart process completed, sending notification")
if self.callback:
self.callback("restart_complete")
return True
except Exception as e:
self.logger.error(f"Error during game restart process: {e}")
# Attempt to notify error
if self.callback:
self.callback("restart_error")
return False
def _terminate_game_process(self):
"""Terminate the game process"""
self.logger.info(f"Attempting to terminate game process '{self.game_process_name}'")
if not HAS_PSUTIL:
self.logger.warning("psutil is not available, cannot terminate process")
return False
process = self._find_game_process()
terminated = False
if process:
try:
self.logger.info(f"Found game process PID: {process.pid}, terminating...")
process.terminate()
try:
process.wait(timeout=5)
self.logger.info(f"Process {process.pid} terminated successfully (terminate)")
terminated = True
except psutil.TimeoutExpired:
self.logger.warning(f"Process {process.pid} did not terminate within 5s (terminate), attempting force kill")
process.kill()
process.wait(timeout=5)
self.logger.info(f"Process {process.pid} force killed (kill)")
terminated = True
except Exception as e:
self.logger.error(f"Error terminating process: {e}")
else:
self.logger.warning(f"No running process found with name '{self.game_process_name}'")
return terminated
def _start_game_process(self):
"""Start the game process"""
if not self.game_path:
self.logger.error("Game executable path not set, cannot start")
return False
self.logger.info(f"Starting game: {self.game_path}")
try:
if sys.platform == "win32":
os.startfile(self.game_path)
self.logger.info("Called os.startfile to launch game")
return True
else:
# Use subprocess.Popen for non-Windows platforms
# Ensure it runs detached if possible, or handle appropriately
subprocess.Popen([self.game_path], start_new_session=True) # Attempt detached start
self.logger.info("Called subprocess.Popen to launch game")
return True
except FileNotFoundError:
self.logger.error(f"Startup error: Game launcher '{self.game_path}' not found")
except OSError as ose:
self.logger.error(f"Startup error (OSError): {ose} - Check path and permissions", exc_info=True)
except Exception as e:
self.logger.error(f"Unexpected error starting game: {e}", exc_info=True)
return False
def restart_now(self):
"""Perform an immediate restart"""
self.logger.info("Manually triggering game restart")
result = self._perform_restart()
# Reset the timer if scheduled restart is enabled
if self.enable_restart and self.restart_interval > 0:
self.next_restart_time = time.time() + (self.restart_interval * 60)
self.logger.info(f"Restart timer reset. Next restart in {self.restart_interval} minutes")
return result
def update_config(self, config_data=None, remote_data=None):
"""Update configuration settings"""
if config_data:
old_config = self.config_data
self.config_data = config_data
# Update key settings
self.window_title = self.config_data.get("GAME_WINDOW_CONFIG", {}).get("WINDOW_TITLE", self.window_title)
self.enable_restart = self.config_data.get("GAME_WINDOW_CONFIG", {}).get("ENABLE_SCHEDULED_RESTART", self.enable_restart)
self.restart_interval = self.config_data.get("GAME_WINDOW_CONFIG", {}).get("RESTART_INTERVAL_MINUTES", self.restart_interval)
self.game_path = self.config_data.get("GAME_WINDOW_CONFIG", {}).get("GAME_EXECUTABLE_PATH", self.game_path)
self.window_x = self.config_data.get("GAME_WINDOW_CONFIG", {}).get("GAME_WINDOW_X", self.window_x)
self.window_y = self.config_data.get("GAME_WINDOW_CONFIG", {}).get("GAME_WINDOW_Y", self.window_y)
self.window_width = self.config_data.get("GAME_WINDOW_CONFIG", {}).get("GAME_WINDOW_WIDTH", self.window_width)
self.window_height = self.config_data.get("GAME_WINDOW_CONFIG", {}).get("GAME_WINDOW_HEIGHT", self.window_height)
self.monitor_interval = self.config_data.get("GAME_WINDOW_CONFIG", {}).get("MONITOR_INTERVAL_SECONDS", self.monitor_interval)
# Reset scheduled restart timer if parameters changed
if self.running and self.enable_restart and self.restart_interval > 0:
old_interval = old_config.get("GAME_WINDOW_CONFIG", {}).get("RESTART_INTERVAL_MINUTES", 60)
if self.restart_interval != old_interval:
self.next_restart_time = time.time() + (self.restart_interval * 60)
self.logger.info(f"Restart interval updated to {self.restart_interval} minutes, next restart reset")
if remote_data:
self.remote_data = remote_data
old_process_name = self.game_process_name
self.game_process_name = self.remote_data.get("GAME_PROCESS_NAME", old_process_name)
if self.game_process_name != old_process_name:
self.logger.info(f"Game process name updated to '{self.game_process_name}'")
self.logger.info("GameMonitor configuration updated")
# Provide simple external API functions
def create_game_monitor(config_data, remote_data=None, logger=None, callback=None):
"""Create a game monitor instance"""
return GameMonitor(config_data, remote_data, logger, callback)
def stop_all_monitors():
"""Attempt to stop all created monitors (global cleanup)"""
# This function could be implemented if instance references are stored.
# In the current design, each monitor needs to be stopped individually.
pass
# Functionality when run standalone (similar to original game_monitor.py)
if __name__ == "__main__":
# Set up basic logging
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
logger = logging.getLogger("GameManagerStandalone")
# Load settings from config.py
try:
import config
logger.info("Loaded config.py")
# Build basic configuration dictionary
config_data = {
"GAME_WINDOW_CONFIG": {
"WINDOW_TITLE": config.WINDOW_TITLE,
"ENABLE_SCHEDULED_RESTART": config.ENABLE_SCHEDULED_RESTART,
"RESTART_INTERVAL_MINUTES": config.RESTART_INTERVAL_MINUTES,
"GAME_EXECUTABLE_PATH": config.GAME_EXECUTABLE_PATH,
"GAME_WINDOW_X": config.GAME_WINDOW_X,
"GAME_WINDOW_Y": config.GAME_WINDOW_Y,
"GAME_WINDOW_WIDTH": config.GAME_WINDOW_WIDTH,
"GAME_WINDOW_HEIGHT": config.GAME_WINDOW_HEIGHT,
"MONITOR_INTERVAL_SECONDS": config.MONITOR_INTERVAL_SECONDS
}
}
# Define a callback for standalone execution
def standalone_callback(action):
"""Send JSON signal via standard output"""
logger.info(f"Sending signal: {action}")
signal_data = {'action': action}
try:
json_signal = json.dumps(signal_data)
print(json_signal, flush=True)
logger.info(f"Signal sent: {action}")
except Exception as e:
logger.error(f"Failed to send signal '{action}': {e}")
# Create and start the monitor
monitor = GameMonitor(config_data, logger=logger, callback=standalone_callback)
monitor.start()
# Keep the program running
try:
logger.info("Game monitoring started. Press Ctrl+C to stop.")
while True:
time.sleep(1)
except KeyboardInterrupt:
logger.info("Ctrl+C received, stopping...")
finally:
monitor.stop()
logger.info("Game monitoring stopped")
except ImportError:
logger.error("Could not load config.py. Ensure it exists and contains necessary settings.")
sys.exit(1)
except Exception as e:
logger.error(f"Error starting game monitoring: {e}", exc_info=True)
sys.exit(1)

View File

@ -1,303 +0,0 @@
#!/usr/bin/env python
"""
Game Window Monitor Module
Continuously monitors the game window specified in the config,
ensuring it stays at the configured position, size, and remains topmost.
"""
import time
import datetime # Added
import subprocess # Added
import psutil # Added
import sys # Added
import json # Added
import os # Added for basename
import pygetwindow as gw
import win32gui
import win32con
import config
import logging
# import multiprocessing # Keep for Pipe/Queue if needed later, though using stdio now
# NOTE: config.py should handle dotenv loading. This script only imports values.
# --- Setup Logging ---
monitor_logger = logging.getLogger('GameMonitor')
monitor_logger.setLevel(logging.INFO) # Set level for the logger
log_formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
# Create handler for stderr
stderr_handler = logging.StreamHandler(sys.stderr) # Explicitly use stderr
stderr_handler.setFormatter(log_formatter)
# Add handler to the logger
if not monitor_logger.hasHandlers(): # Avoid adding multiple handlers if run multiple times
monitor_logger.addHandler(stderr_handler)
monitor_logger.propagate = False # Prevent propagation to root logger if basicConfig was called elsewhere
# --- Helper Functions ---
def restart_game_process():
"""Finds and terminates the existing game process, then restarts it."""
monitor_logger.info("嘗試重啟遊戲進程。(Attempting to restart game process.)")
game_path = config.GAME_EXECUTABLE_PATH
if not game_path or not os.path.exists(os.path.dirname(game_path)): # Basic check
monitor_logger.error(f"遊戲執行檔路徑 '{game_path}' 無效或目錄不存在,無法重啟。(Game executable path '{game_path}' is invalid or directory does not exist, cannot restart.)")
return
target_process_name = "LastWar.exe" # Correct process name
launcher_path = config.GAME_EXECUTABLE_PATH # Keep launcher path for restarting
monitor_logger.info(f"尋找名稱為 '{target_process_name}' 的遊戲進程。(Looking for game process named '{target_process_name}')")
terminated = False
process_found = False
for proc in psutil.process_iter(['pid', 'name', 'exe']):
try:
proc_info = proc.info
proc_name = proc_info.get('name')
if proc_name == target_process_name:
process_found = True
monitor_logger.info(f"找到遊戲進程 PID: {proc_info['pid']},名稱: {proc_name}。正在終止...(Found game process PID: {proc_info['pid']}, Name: {proc_name}. Terminating...)")
proc.terminate()
try:
proc.wait(timeout=5)
monitor_logger.info(f"進程 {proc_info['pid']} 已成功終止 (terminate)。(Process {proc_info['pid']} terminated successfully (terminate).)")
terminated = True
except psutil.TimeoutExpired:
monitor_logger.warning(f"進程 {proc_info['pid']} 未能在 5 秒內終止 (terminate),嘗試強制結束 (kill)。(Process {proc_info['pid']} did not terminate in 5s (terminate), attempting kill.)")
proc.kill()
proc.wait(timeout=5) # Wait for kill with timeout
monitor_logger.info(f"進程 {proc_info['pid']} 已強制結束 (kill)。(Process {proc_info['pid']} killed.)")
terminated = True
except Exception as wait_kill_err:
monitor_logger.error(f"等待進程 {proc_info['pid']} 強制結束時出錯: {wait_kill_err}", exc_info=False)
# Removed Termination Verification - Rely on main loop for eventual state correction
monitor_logger.info(f"已處理匹配的進程 PID: {proc_info['pid']},停止搜索。(Processed matching process PID: {proc_info['pid']}, stopping search.)")
break # Exit the loop once a process is handled
except (psutil.NoSuchProcess, psutil.AccessDenied, psutil.ZombieProcess):
pass # Process might have already exited, access denied, or is a zombie
except Exception as e:
pid_str = proc.pid if hasattr(proc, 'pid') else 'N/A'
monitor_logger.error(f"檢查或終止進程 PID:{pid_str} 時出錯: {e}", exc_info=False)
if process_found and not terminated:
monitor_logger.error("找到遊戲進程但未能成功終止它。(Found game process but failed to terminate it successfully.)")
elif not process_found:
monitor_logger.warning(f"未找到名稱為 '{target_process_name}' 的正在運行的進程。(No running process named '{target_process_name}' was found.)")
# Wait a moment before restarting, use the launcher path from config
time.sleep(2)
if not launcher_path or not os.path.exists(os.path.dirname(launcher_path)):
monitor_logger.error(f"遊戲啟動器路徑 '{launcher_path}' 無效或目錄不存在,無法啟動。(Game launcher path '{launcher_path}' is invalid or directory does not exist, cannot launch.)")
return
monitor_logger.info(f"正在使用啟動器啟動遊戲: {launcher_path} (Launching game using launcher: {launcher_path})")
try:
if sys.platform == "win32":
os.startfile(launcher_path)
monitor_logger.info("已調用 os.startfile 啟動遊戲。(os.startfile called to launch game.)")
else:
subprocess.Popen([launcher_path])
monitor_logger.info("已調用 subprocess.Popen 啟動遊戲。(subprocess.Popen called to launch game.)")
except FileNotFoundError:
monitor_logger.error(f"啟動錯誤:找不到遊戲啟動器 '{launcher_path}'。(Launch Error: Game launcher not found at '{launcher_path}'.)")
except OSError as ose:
monitor_logger.error(f"啟動錯誤 (OSError): {ose} - 檢查路徑和權限。(Launch Error (OSError): {ose} - Check path and permissions.)", exc_info=True)
except Exception as e:
monitor_logger.error(f"啟動遊戲時發生未預期錯誤: {e}", exc_info=True)
# Don't return False here, let the process continue to send resume signal
# Removed Startup Verification - Rely on main loop for eventual state correction
# Always return True (or nothing) to indicate the attempt was made
return # Or return True, doesn't matter much now
def perform_scheduled_restart():
"""Handles the sequence of pausing UI, restarting game, resuming UI."""
monitor_logger.info("開始執行定時重啟流程。(Starting scheduled restart sequence.)")
# Removed pause_ui signal - UI will handle its own pause/resume based on restart_complete
try:
# 1. Attempt to restart the game (no verification)
monitor_logger.info("嘗試執行遊戲重啟。(Attempting game restart process.)")
restart_game_process() # Fire and forget restart attempt
monitor_logger.info("遊戲重啟嘗試已執行。(Game restart attempt executed.)")
# 2. Wait fixed time after restart attempt
monitor_logger.info("等待 30 秒讓遊戲啟動(無驗證)。(Waiting 30 seconds for game to launch (no verification)...)")
time.sleep(30) # Fixed wait
except Exception as restart_err:
monitor_logger.error(f"執行 restart_game_process 時發生未預期錯誤: {restart_err}", exc_info=True)
# Continue to finally block even on error
finally:
# 3. Signal main process that restart attempt is complete via stdout
monitor_logger.info("發送重啟完成訊號。(Sending restart complete signal.)")
restart_complete_signal_data = {'action': 'restart_complete'}
try:
json_signal = json.dumps(restart_complete_signal_data)
print(json_signal, flush=True)
monitor_logger.info("已發送重啟完成訊號。(Sent restart complete signal.)")
except Exception as e:
monitor_logger.error(f"發送重啟完成訊號 '{json_signal}' 失敗: {e}", exc_info=True) # Log signal data on error
monitor_logger.info("定時重啟流程(包括 finally 塊)執行完畢。(Scheduled restart sequence (including finally block) finished.)")
# Configure logger (basic example, adjust as needed)
# (Logging setup moved earlier)
def find_game_window(title=config.WINDOW_TITLE):
"""Attempts to find the game window by its title."""
try:
windows = gw.getWindowsWithTitle(title)
if windows:
return windows[0]
except Exception as e:
# Log errors if a logger was configured
# monitor_logger.error(f"Error finding window '{title}': {e}")
pass # Keep silent if window not found during normal check
return None
def monitor_game_window():
"""The main monitoring loop. Now runs directly, not in a thread."""
monitor_logger.info("遊戲視窗監控腳本已啟動。(Game window monitoring script started.)")
last_adjustment_message = "" # Track last message to avoid spam
next_restart_time = None
# Initialize scheduled restart timer if enabled
if config.ENABLE_SCHEDULED_RESTART and config.RESTART_INTERVAL_MINUTES > 0:
interval_seconds = config.RESTART_INTERVAL_MINUTES * 60
next_restart_time = time.time() + interval_seconds
monitor_logger.info(f"已啟用定時重啟,首次重啟將在 {config.RESTART_INTERVAL_MINUTES} 分鐘後執行。(Scheduled restart enabled. First restart in {config.RESTART_INTERVAL_MINUTES} minutes.)")
else:
monitor_logger.info("未啟用定時重啟功能。(Scheduled restart is disabled.)")
while True: # Run indefinitely until terminated externally
# --- Scheduled Restart Check ---
if next_restart_time and time.time() >= next_restart_time:
monitor_logger.info("到達預定重啟時間。(Scheduled restart time reached.)")
perform_scheduled_restart()
# Reset timer for the next interval
interval_seconds = config.RESTART_INTERVAL_MINUTES * 60
next_restart_time = time.time() + interval_seconds
monitor_logger.info(f"重啟計時器已重置,下次重啟將在 {config.RESTART_INTERVAL_MINUTES} 分鐘後執行。(Restart timer reset. Next restart in {config.RESTART_INTERVAL_MINUTES} minutes.)")
# Continue to next loop iteration after restart sequence
time.sleep(config.MONITOR_INTERVAL_SECONDS) # Add a small delay before next check
continue
# --- Regular Window Monitoring ---
window = find_game_window()
adjustment_made = False
current_message = ""
if window:
try:
hwnd = window._hWnd # Get the window handle for win32 functions
# 1. Check and Adjust Position/Size
current_pos = (window.left, window.top)
current_size = (window.width, window.height)
target_pos = (config.GAME_WINDOW_X, config.GAME_WINDOW_Y)
target_size = (config.GAME_WINDOW_WIDTH, config.GAME_WINDOW_HEIGHT)
if current_pos != target_pos or current_size != target_size:
window.moveTo(target_pos[0], target_pos[1])
window.resizeTo(target_size[0], target_size[1])
# Verify if move/resize was successful before logging
time.sleep(0.1) # Give window time to adjust
window.activate() # Bring window to foreground before checking again
time.sleep(0.1)
new_pos = (window.left, window.top)
new_size = (window.width, window.height)
if new_pos == target_pos and new_size == target_size:
current_message += f"已將遊戲視窗調整至位置 ({target_pos[0]},{target_pos[1]}) 大小 {target_size[0]}x{target_size[1]}。(Adjusted game window to position {target_pos} size {target_size}.) "
adjustment_made = True
else:
# Log failure if needed
# monitor_logger.warning(f"Failed to adjust window. Current: {new_pos} {new_size}, Target: {target_pos} {target_size}")
pass # Keep silent on failure for now
# 2. Check and Bring to Foreground/Activate (Improved Logic)
current_foreground_hwnd = win32gui.GetForegroundWindow()
if current_foreground_hwnd != hwnd:
try:
# Use HWND_TOP instead of HWND_TOPMOST to bring it above others without forcing always-on-top
win32gui.SetWindowPos(hwnd, win32con.HWND_TOP, 0, 0, 0, 0,
win32con.SWP_NOMOVE | win32con.SWP_NOSIZE)
# Make window the foreground window (with focus)
# Note: This might still fail due to Windows foreground restrictions
win32gui.SetForegroundWindow(hwnd)
# Verify window is active by checking foreground window
time.sleep(0.1) # Brief pause to let operation complete
foreground_hwnd = win32gui.GetForegroundWindow()
if foreground_hwnd == hwnd:
current_message += "已將遊戲視窗提升到前景並設為焦點。(Brought game window to foreground with focus.) "
adjustment_made = True
else:
# Optional: Add a fallback for versions of Windows with stricter foreground rules
monitor_logger.warning("SetForegroundWindow 未能成功,嘗試備用方法 window.activate()。(SetForegroundWindow failed, trying fallback window.activate())")
try:
window.activate() # Try pygetwindow's activate method as backup
time.sleep(0.1) # Pause after activate
if win32gui.GetForegroundWindow() == hwnd:
current_message += "已透過備用方法將遊戲視窗設為焦點。(Set game window focus via fallback method.) "
adjustment_made = True
else:
monitor_logger.warning("備用方法 window.activate() 也未能成功。(Fallback window.activate() also failed.)")
except Exception as activate_err:
monitor_logger.warning(f"備用方法 window.activate() 出錯: {activate_err}")
except Exception as focus_err:
# Log errors during the focus attempt
monitor_logger.warning(f"設置視窗焦點時出錯: {focus_err}")
except gw.PyGetWindowException as e:
# Log PyGetWindowException specifically, might indicate window closed during check
monitor_logger.warning(f"監控循環中無法訪問視窗屬性 (可能已關閉): {e} (Could not access window properties in monitor loop (may be closed): {e})")
except Exception as e:
# Log other exceptions during monitoring
monitor_logger.error(f"監控遊戲視窗時發生未預期錯誤: {e} (Unexpected error during game window monitoring: {e})", exc_info=True)
# Log adjustment message only if an adjustment was made and it's different from the last one
# This should NOT print JSON signals
if adjustment_made and current_message and current_message != last_adjustment_message:
# Log the adjustment message instead of printing to stdout
monitor_logger.info(f"[GameMonitor] {current_message.strip()}")
last_adjustment_message = current_message
elif not window:
# Reset last message if window disappears
last_adjustment_message = ""
# Wait before the next check
time.sleep(config.MONITOR_INTERVAL_SECONDS)
# This part is theoretically unreachable in the new design as the loop is infinite
# and termination is handled externally by the parent process (main.py).
# monitor_logger.info("遊戲視窗監控腳本已停止。(Game window monitoring script stopped.)")
# Example usage (if run directly)
if __name__ == '__main__':
monitor_logger.info("直接運行 game_monitor.py。(Running game_monitor.py directly.)")
monitor_logger.info(f"將監控標題為 '{config.WINDOW_TITLE}' 的視窗。(Will monitor window with title '{config.WINDOW_TITLE}')")
monitor_logger.info(f"目標位置: ({config.GAME_WINDOW_X}, {config.GAME_WINDOW_Y}), 目標大小: {config.GAME_WINDOW_WIDTH}x{config.GAME_WINDOW_HEIGHT}")
monitor_logger.info(f"檢查間隔: {config.MONITOR_INTERVAL_SECONDS} 秒。(Check interval: {config.MONITOR_INTERVAL_SECONDS} seconds.)")
if config.ENABLE_SCHEDULED_RESTART:
monitor_logger.info(f"定時重啟已啟用,間隔: {config.RESTART_INTERVAL_MINUTES} 分鐘。(Scheduled restart enabled, interval: {config.RESTART_INTERVAL_MINUTES} minutes.)")
else:
monitor_logger.info("定時重啟已禁用。(Scheduled restart disabled.)")
monitor_logger.info("腳本將持續運行,請從啟動它的終端使用 Ctrl+C 或由父進程終止。(Script will run continuously. Stop with Ctrl+C from the launching terminal or termination by parent process.)")
try:
monitor_game_window() # Start the main loop directly
except KeyboardInterrupt:
monitor_logger.info("收到 Ctrl+C正在退出...(Received Ctrl+C, exiting...)")
except Exception as e:
monitor_logger.critical(f"監控過程中發生致命錯誤: {e}", exc_info=True)
sys.exit(1) # Exit with error code
finally:
monitor_logger.info("Game Monitor 腳本執行完畢。(Game Monitor script finished.)")

152
main.py
View File

@ -30,7 +30,6 @@ import llm_interaction
# Import UI module
import ui_interaction
import chroma_client
# import game_monitor # No longer importing, will run as subprocess
import subprocess # Import subprocess module
import signal
import platform
@ -65,9 +64,6 @@ trigger_queue: ThreadSafeQueue = ThreadSafeQueue() # UI Thread -> Main Loop
command_queue: ThreadSafeQueue = ThreadSafeQueue() # Main Loop -> UI Thread
# --- End Change ---
ui_monitor_task: asyncio.Task | None = None # To track the UI monitor task
game_monitor_process: subprocess.Popen | None = None # To store the game monitor subprocess
monitor_reader_task: asyncio.Future | None = None # Store the future from run_in_executor
stop_reader_event = threading.Event() # Event to signal the reader thread to stop
# --- Keyboard Shortcut State ---
script_paused = False
@ -149,70 +145,6 @@ def keyboard_listener():
# --- End Keyboard Shortcut Handlers ---
# --- Game Monitor Signal Reader (Threaded Blocking Version) ---
def read_monitor_output(process: subprocess.Popen, queue: ThreadSafeQueue, loop: asyncio.AbstractEventLoop, stop_event: threading.Event):
"""Runs in a separate thread, reads stdout blocking, parses JSON, and puts commands in the queue."""
print("Game monitor output reader thread started.")
try:
while not stop_event.is_set():
if not process.stdout:
print("[Monitor Reader Thread] Subprocess stdout is None. Exiting thread.")
break
try:
# Blocking read - this is fine in a separate thread
line = process.stdout.readline()
except ValueError:
# Can happen if the pipe is closed during readline
print("[Monitor Reader Thread] ValueError on readline (pipe likely closed). Exiting thread.")
break
if not line:
# EOF reached (process terminated)
print("[Monitor Reader Thread] EOF reached on stdout. Exiting thread.")
break
line = line.strip()
if line:
# Log raw line immediately
print(f"[Monitor Reader Thread] Received raw line: '{line}'")
try:
data = json.loads(line)
action = data.get('action')
print(f"[Monitor Reader Thread] Parsed action: '{action}'") # Log parsed action
if action == 'pause_ui':
command = {'action': 'pause'}
print(f"[Monitor Reader Thread] Preparing to queue command: {command}") # Log before queueing
loop.call_soon_threadsafe(queue.put_nowait, command)
print("[Monitor Reader Thread] Pause command queued.") # Log after queueing
elif action == 'resume_ui':
# Removed direct resume_ui handling - ui_interaction will handle pause/resume based on restart_complete
print("[Monitor Reader Thread] Received old 'resume_ui' signal, ignoring.")
elif action == 'restart_complete':
command = {'action': 'handle_restart_complete'}
print(f"[Monitor Reader Thread] Received 'restart_complete' signal, preparing to queue command: {command}")
try:
loop.call_soon_threadsafe(queue.put_nowait, command)
print("[Monitor Reader Thread] 'handle_restart_complete' command queued.")
except Exception as q_err:
print(f"[Monitor Reader Thread] Error putting 'handle_restart_complete' command in queue: {q_err}")
else:
print(f"[Monitor Reader Thread] Received unknown action from monitor: {action}")
except json.JSONDecodeError:
print(f"[Monitor Reader Thread] ERROR: Could not decode JSON from monitor: '{line}'")
# Log the raw line that failed to parse
# print(f"[Monitor Reader Thread] Raw line that failed JSON decode: '{line}'") # Already logged raw line earlier
except Exception as e:
print(f"[Monitor Reader Thread] Error processing monitor output: {e}")
# No sleep needed here as readline() is blocking
except Exception as e:
# Catch broader errors in the thread loop itself
print(f"[Monitor Reader Thread] Thread loop error: {e}")
finally:
print("Game monitor output reader thread stopped.")
# --- End Game Monitor Signal Reader ---
# --- Chat Logging Function ---
def log_chat_interaction(user_name: str, user_message: str, bot_name: str, bot_message: str, bot_thoughts: str | None = None):
"""Logs the chat interaction, including optional bot thoughts, to a date-stamped file if enabled."""
@ -318,7 +250,7 @@ if platform.system() == "Windows" and win32api and win32con:
# --- Cleanup Function ---
async def shutdown():
"""Gracefully closes connections and stops monitoring tasks/processes."""
global wolfhart_persona_details, ui_monitor_task, shutdown_requested, game_monitor_process, monitor_reader_task # Add monitor_reader_task
global wolfhart_persona_details, ui_monitor_task, shutdown_requested
# Ensure shutdown is requested if called externally (e.g., Ctrl+C)
if not shutdown_requested:
print("Shutdown initiated externally (e.g., Ctrl+C).")
@ -338,42 +270,7 @@ async def shutdown():
except Exception as e:
print(f"Error while waiting for UI monitoring task cancellation: {e}")
# 1b. Signal and Wait for Monitor Reader Thread
if monitor_reader_task: # Check if the future exists
if not stop_reader_event.is_set():
print("Signaling monitor output reader thread to stop...")
stop_reader_event.set()
# Wait for the thread to finish (the future returned by run_in_executor)
# This might block briefly, but it's necessary to ensure clean thread shutdown
# We don't await it directly in the async shutdown, but check if it's done
# A better approach might be needed if the thread blocks indefinitely
print("Waiting for monitor output reader thread to finish (up to 2s)...")
try:
# Wait for the future to complete with a timeout
await asyncio.wait_for(monitor_reader_task, timeout=2.0)
print("Monitor output reader thread finished.")
except asyncio.TimeoutError:
print("Warning: Monitor output reader thread did not finish within timeout.")
except asyncio.CancelledError:
print("Monitor output reader future was cancelled.") # Should not happen if we don't cancel it
except Exception as e:
print(f"Error waiting for monitor reader thread future: {e}")
# 2. Terminate Game Monitor Subprocess (after signaling reader thread)
if game_monitor_process:
print("Terminating game monitor subprocess...")
try:
game_monitor_process.terminate()
# Optionally wait for a short period or check return code
# game_monitor_process.wait(timeout=1)
print("Game monitor subprocess terminated.")
except Exception as e:
print(f"Error terminating game monitor subprocess: {e}")
finally:
game_monitor_process = None # Clear the reference
# 3. Close MCP connections via AsyncExitStack
# 2. Close MCP connections via AsyncExitStack
# This will trigger the __aexit__ method of stdio_client contexts,
# which we assume handles terminating the server subprocesses it started.
print(f"Closing MCP Server connections (via AsyncExitStack)...")
@ -555,7 +452,7 @@ def initialize_memory_system():
# --- Main Async Function ---
async def run_main_with_exit_stack():
"""Initializes connections, loads persona, starts UI monitor and main processing loop."""
global initialization_successful, main_task, loop, wolfhart_persona_details, trigger_queue, ui_monitor_task, shutdown_requested, script_paused, command_queue, game_monitor_process, monitor_reader_task # Add monitor_reader_task to globals
global initialization_successful, main_task, loop, wolfhart_persona_details, trigger_queue, ui_monitor_task, shutdown_requested, script_paused, command_queue
try:
# 1. Load Persona Synchronously (before async loop starts)
load_persona_from_file() # Corrected function
@ -594,48 +491,7 @@ async def run_main_with_exit_stack():
ui_monitor_task = monitor_task # Store task reference for shutdown
# Note: UI task cancellation is handled in shutdown()
# 5b. Start Game Window Monitoring as a Subprocess
# global game_monitor_process, monitor_reader_task # Already declared global at function start
print("\n--- Starting Game Window monitoring as a subprocess ---")
try:
# Use sys.executable to ensure the same Python interpreter is used
# Capture stdout to read signals
game_monitor_process = subprocess.Popen(
[sys.executable, 'game_monitor.py'],
stdout=subprocess.PIPE, # Capture stdout
stderr=subprocess.PIPE, # Capture stderr for logging/debugging
text=True, # Decode stdout/stderr as text (UTF-8 by default)
bufsize=1, # Line buffered
# Ensure process creation flags are suitable for Windows if needed
# creationflags=subprocess.CREATE_NO_WINDOW # Example: Hide console window
)
print(f"Game monitor subprocess started (PID: {game_monitor_process.pid}).")
# Start the thread to read monitor output if process started successfully
if game_monitor_process.stdout:
# Run the blocking reader function in a separate thread using the default executor
monitor_reader_task = loop.run_in_executor(
None, # Use default ThreadPoolExecutor
read_monitor_output, # The function to run
game_monitor_process, # Arguments for the function...
command_queue,
loop,
stop_reader_event # Pass the stop event
)
print("Monitor output reader thread submitted to executor.")
else:
print("Error: Could not access game monitor subprocess stdout.")
monitor_reader_task = None
# Optionally, start a task to read stderr as well for debugging
# stderr_reader_task = loop.create_task(read_stderr(game_monitor_process), name="monitor_stderr_reader")
except FileNotFoundError:
print("Error: 'game_monitor.py' not found. Cannot start game monitor subprocess.")
game_monitor_process = None
except Exception as e:
print(f"Error starting game monitor subprocess: {e}")
game_monitor_process = None
# 5b. Game Window Monitoring is now handled by Setup.py
# 6. Start the main processing loop (non-blocking check on queue)