Add Game Monitor for window position tracking and auto-restart
This commit is contained in:
parent
805662943f
commit
94e3b55136
@ -50,13 +50,13 @@ Wolf Chat 是一個基於 MCP (Modular Capability Provider) 框架的聊天機
|
|||||||
- 包含外觀、說話風格、個性特點等資訊
|
- 包含外觀、說話風格、個性特點等資訊
|
||||||
- 提供給 LLM 以確保角色扮演一致性
|
- 提供給 LLM 以確保角色扮演一致性
|
||||||
|
|
||||||
7. **視窗設定工具 (window-setup-script.py)**
|
7. **遊戲視窗監控模組 (game_monitor.py)** (取代 window-setup-script.py 和舊的 window-monitor-script.py)
|
||||||
- 輔助工具,用於設置遊戲視窗的位置和大小
|
- 持續監控遊戲視窗 (`config.WINDOW_TITLE`)。
|
||||||
- 方便開發階段截取 UI 元素樣本
|
- 確保視窗維持在設定檔 (`config.py`) 中指定的位置 (`GAME_WINDOW_X`, `GAME_WINDOW_Y`) 和大小 (`GAME_WINDOW_WIDTH`, `GAME_WINDOW_HEIGHT`)。
|
||||||
8. **視窗監視工具 (window-monitor-script.py)**
|
- 確保視窗維持在最上層 (Always on Top)。
|
||||||
- (新增) 強化腳本,用於持續監視遊戲視窗
|
- **作為獨立進程運行**:由 `main.py` 使用 `subprocess.Popen` 啟動,以隔離執行環境,確保縮放行為一致。
|
||||||
- 確保目標視窗維持在最上層 (Always on Top)
|
- **生命週期管理**:由 `main.py` 在啟動時創建,並在 `shutdown` 過程中嘗試終止 (`terminate`)。
|
||||||
- 自動將視窗移回指定的位置
|
- 僅在進行調整時打印訊息。
|
||||||
|
|
||||||
### 資料流程
|
### 資料流程
|
||||||
|
|
||||||
@ -165,7 +165,10 @@ Wolf Chat 是一個基於 MCP (Modular Capability Provider) 框架的聊天機
|
|||||||
1. **API 設定**:通過 .env 文件或環境變數設置 API 密鑰
|
1. **API 設定**:通過 .env 文件或環境變數設置 API 密鑰
|
||||||
2. **MCP 服務器配置**:在 config.py 中配置要連接的 MCP 服務器
|
2. **MCP 服務器配置**:在 config.py 中配置要連接的 MCP 服務器
|
||||||
3. **UI 樣本**:需要提供特定遊戲界面元素的截圖模板
|
3. **UI 樣本**:需要提供特定遊戲界面元素的截圖模板
|
||||||
4. **視窗位置**:可使用 window-setup-script.py 調整遊戲視窗位置
|
4. **遊戲視窗設定**:
|
||||||
|
- 遊戲執行檔路徑 (`GAME_EXECUTABLE_PATH`):用於未來可能的自動啟動功能。
|
||||||
|
- 目標視窗位置與大小 (`GAME_WINDOW_X`, `GAME_WINDOW_Y`, `GAME_WINDOW_WIDTH`, `GAME_WINDOW_HEIGHT`):由 `game_monitor.py` 使用。
|
||||||
|
- 監控間隔 (`MONITOR_INTERVAL_SECONDS`):`game_monitor.py` 檢查視窗狀態的頻率。
|
||||||
|
|
||||||
## 最近改進(2025-04-17)
|
## 最近改進(2025-04-17)
|
||||||
|
|
||||||
|
|||||||
46
config.py
46
config.py
@ -37,19 +37,29 @@ exa_config_arg_string_single_dump = json.dumps(exa_config_dict) # Use this one
|
|||||||
|
|
||||||
# --- MCP Server Configuration ---
|
# --- MCP Server Configuration ---
|
||||||
MCP_SERVERS = {
|
MCP_SERVERS = {
|
||||||
#"exa": { # Temporarily commented out to prevent blocking startup
|
"exa": { # Temporarily commented out to prevent blocking startup
|
||||||
# "command": "cmd",
|
"command": "cmd",
|
||||||
## "args": [
|
"args": [
|
||||||
# "/c",
|
"/c",
|
||||||
# "npx",
|
"npx",
|
||||||
# "-y",
|
"-y",
|
||||||
# "@smithery/cli@latest",
|
"@smithery/cli@latest",
|
||||||
# "run",
|
"run",
|
||||||
# "exa",
|
"exa",
|
||||||
# "--config",
|
"--config",
|
||||||
# # Pass the dynamically created config string with the environment variable key
|
# Pass the dynamically created config string with the environment variable key
|
||||||
# exa_config_arg_string_single_dump # Use the single dump variable
|
exa_config_arg_string_single_dump # Use the single dump variable
|
||||||
|
],
|
||||||
|
},
|
||||||
|
#"exa": {
|
||||||
|
# "command": "npx",
|
||||||
|
# "args": [
|
||||||
|
# "Z:/mcp/Server/exa-mcp-server/build/index.js",
|
||||||
|
# "--tools=web_search,research_paper_search,twitter_search,company_research,crawling,competitor_finder"
|
||||||
# ],
|
# ],
|
||||||
|
# "env": {
|
||||||
|
# "EXA_API_KEY": EXA_API_KEY
|
||||||
|
# }
|
||||||
#},
|
#},
|
||||||
#"github.com/modelcontextprotocol/servers/tree/main/src/memory": {
|
#"github.com/modelcontextprotocol/servers/tree/main/src/memory": {
|
||||||
# "command": "npx",
|
# "command": "npx",
|
||||||
@ -105,9 +115,19 @@ LOG_DIR = "chat_logs" # Directory to store chat logs
|
|||||||
PERSONA_NAME = "Wolfhart"
|
PERSONA_NAME = "Wolfhart"
|
||||||
# PERSONA_RESOURCE_URI = "persona://wolfhart/details" # Now using local file instead
|
# PERSONA_RESOURCE_URI = "persona://wolfhart/details" # Now using local file instead
|
||||||
|
|
||||||
# Game window title (used in ui_interaction.py)
|
# Game window title (used in ui_interaction.py and game_monitor.py)
|
||||||
WINDOW_TITLE = "Last War-Survival Game"
|
WINDOW_TITLE = "Last War-Survival Game"
|
||||||
|
|
||||||
|
# --- Game Monitor Configuration ---
|
||||||
|
ENABLE_SCHEDULED_RESTART = True # 是否啟用定時重啟遊戲功能
|
||||||
|
RESTART_INTERVAL_MINUTES = 60 # 定時重啟的間隔時間(分鐘),預設 4 小時
|
||||||
|
GAME_EXECUTABLE_PATH = r"C:\Users\Bigspring\AppData\Local\TheLastWar\Launch.exe" # Path to the game launcher
|
||||||
|
GAME_WINDOW_X = 50 # Target X position for the game window
|
||||||
|
GAME_WINDOW_Y = 30 # Target Y position for the game window
|
||||||
|
GAME_WINDOW_WIDTH = 600 # Target width for the game window
|
||||||
|
GAME_WINDOW_HEIGHT = 1070 # Target height for the game window
|
||||||
|
MONITOR_INTERVAL_SECONDS = 5 # How often to check the window (in seconds)
|
||||||
|
|
||||||
# --- Print loaded keys for verification (Optional - BE CAREFUL!) ---
|
# --- Print loaded keys for verification (Optional - BE CAREFUL!) ---
|
||||||
# print(f"DEBUG: Loaded OPENAI_API_KEY: {'*' * (len(OPENAI_API_KEY) - 4) + OPENAI_API_KEY[-4:] if OPENAI_API_KEY else 'Not Found'}")
|
# print(f"DEBUG: Loaded OPENAI_API_KEY: {'*' * (len(OPENAI_API_KEY) - 4) + OPENAI_API_KEY[-4:] if OPENAI_API_KEY else 'Not Found'}")
|
||||||
print(f"DEBUG: Loaded EXA_API_KEY: {'*' * (len(EXA_API_KEY) - 4) + EXA_API_KEY[-4:] if EXA_API_KEY else 'Not Found'}") # Uncommented Exa key check
|
print(f"DEBUG: Loaded EXA_API_KEY: {'*' * (len(EXA_API_KEY) - 4) + EXA_API_KEY[-4:] if EXA_API_KEY else 'Not Found'}") # Uncommented Exa key check
|
||||||
|
|||||||
277
game_monitor.py
Normal file
277
game_monitor.py
Normal file
@ -0,0 +1,277 @@
|
|||||||
|
#!/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')
|
||||||
|
# Basic config for direct run, main.py might configure differently
|
||||||
|
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
|
||||||
|
|
||||||
|
# --- 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)
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
def perform_scheduled_restart():
|
||||||
|
"""Handles the sequence of pausing UI, restarting game, resuming UI."""
|
||||||
|
monitor_logger.info("開始執行定時重啟流程。(Starting scheduled restart sequence.)")
|
||||||
|
|
||||||
|
# 1. Signal main process to pause UI monitoring via stdout
|
||||||
|
pause_signal_data = {'action': 'pause_ui'}
|
||||||
|
try:
|
||||||
|
json_signal = json.dumps(pause_signal_data)
|
||||||
|
monitor_logger.info(f"準備發送暫停訊號: {json_signal} (Preparing to send pause signal)") # Log before print
|
||||||
|
print(json_signal, flush=True)
|
||||||
|
monitor_logger.info("已發送暫停 UI 監控訊號。(Sent pause UI monitoring signal.)")
|
||||||
|
except Exception as e:
|
||||||
|
monitor_logger.error(f"發送暫停訊號 '{json_signal}' 失敗: {e}", exc_info=True) # Log signal data on error
|
||||||
|
|
||||||
|
# 2. Wait 1 minute
|
||||||
|
monitor_logger.info("等待 60 秒以暫停 UI。(Waiting 60 seconds for UI pause...)")
|
||||||
|
time.sleep(30)
|
||||||
|
|
||||||
|
# 3. Restart the game
|
||||||
|
restart_game_process()
|
||||||
|
|
||||||
|
# 4. Wait 1 minute
|
||||||
|
monitor_logger.info("等待 60 秒讓遊戲啟動。(Waiting 60 seconds for game to launch...)")
|
||||||
|
time.sleep(30)
|
||||||
|
|
||||||
|
# 5. Signal main process to resume UI monitoring via stdout
|
||||||
|
resume_signal_data = {'action': 'resume_ui'}
|
||||||
|
try:
|
||||||
|
json_signal = json.dumps(resume_signal_data)
|
||||||
|
monitor_logger.info(f"準備發送恢復訊號: {json_signal} (Preparing to send resume signal)") # Log before print
|
||||||
|
print(json_signal, flush=True)
|
||||||
|
monitor_logger.info("已發送恢復 UI 監控訊號。(Sent resume UI monitoring signal.)")
|
||||||
|
except Exception as e:
|
||||||
|
monitor_logger.error(f"發送恢復訊號 '{json_signal}' 失敗: {e}", exc_info=True) # Log signal data on error
|
||||||
|
|
||||||
|
monitor_logger.info("定時重啟流程完成。(Scheduled restart sequence complete.)")
|
||||||
|
# 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 Set Topmost
|
||||||
|
style = win32gui.GetWindowLong(hwnd, win32con.GWL_EXSTYLE)
|
||||||
|
is_topmost = style & win32con.WS_EX_TOPMOST
|
||||||
|
|
||||||
|
if not is_topmost:
|
||||||
|
# Set topmost, -1 for HWND_TOPMOST, flags = SWP_NOMOVE | SWP_NOSIZE
|
||||||
|
win32gui.SetWindowPos(hwnd, win32con.HWND_TOPMOST, 0, 0, 0, 0,
|
||||||
|
win32con.SWP_NOMOVE | win32con.SWP_NOSIZE)
|
||||||
|
# Verify
|
||||||
|
time.sleep(0.1)
|
||||||
|
new_style = win32gui.GetWindowLong(hwnd, win32con.GWL_EXSTYLE)
|
||||||
|
if new_style & win32con.WS_EX_TOPMOST:
|
||||||
|
current_message += "已將遊戲視窗設為最上層。(Set game window to topmost.)"
|
||||||
|
adjustment_made = True
|
||||||
|
else:
|
||||||
|
# Log failure if needed
|
||||||
|
# monitor_logger.warning("Failed to set window to topmost.")
|
||||||
|
pass # Keep silent
|
||||||
|
|
||||||
|
except gw.PyGetWindowException as e:
|
||||||
|
# monitor_logger.warning(f"無法訪問視窗屬性 (可能已關閉): {e} (Could not access window properties (may be closed): {e})")
|
||||||
|
pass # Window might have closed between find and access
|
||||||
|
except Exception as e:
|
||||||
|
monitor_logger.error(f"監控遊戲視窗時發生未預期錯誤: {e} (Unexpected error during game window monitoring: {e})", exc_info=True)
|
||||||
|
|
||||||
|
# Print 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:
|
||||||
|
# monitor_logger.info(f"遊戲視窗狀態已調整: {current_message.strip()}") # Log instead of print
|
||||||
|
# Keep print for now for visibility of adjustments, but ensure ONLY JSON goes for signals
|
||||||
|
# print(f"[GameMonitor] {current_message.strip()}") # REMOVED to prevent non-JSON output
|
||||||
|
monitor_logger.info(f"[GameMonitor] {current_message.strip()}") # Log instead
|
||||||
|
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.)")
|
||||||
@ -100,6 +100,9 @@ Here you need to obtain the conversation memory, impression, and emotional respo
|
|||||||
"thoughts": "Internal analysis..."
|
"thoughts": "Internal analysis..."
|
||||||
}
|
}
|
||||||
|
|
||||||
|
**4. Other situation
|
||||||
|
- You should check related memories when Users mention [capital_position], [capital_administrator_role], [server_hierarchy], [last_war], [winter_war], [excavations], [blueprints], [honor_points], [golden_eggs], or [diamonds], as these represent key game mechanics and systems within Last War that have specific strategic implications and player evaluation metrics.
|
||||||
|
|
||||||
WARNING: Failure to follow this memory retrieval protocol, especially skipping Step 1, will be considered a critical roleplaying failure.
|
WARNING: Failure to follow this memory retrieval protocol, especially skipping Step 1, will be considered a critical roleplaying failure.
|
||||||
===== END OF MANDATORY MEMORY PROTOCOL =====
|
===== END OF MANDATORY MEMORY PROTOCOL =====
|
||||||
"""
|
"""
|
||||||
@ -115,6 +118,7 @@ You have access to several tools: Web Search and Memory Management tools.
|
|||||||
|
|
||||||
**CORE IDENTITY AND TOOL USAGE:**
|
**CORE IDENTITY AND TOOL USAGE:**
|
||||||
- You ARE Wolfhart - an intelligent, calm, and strategic mastermind who serves as a member of server #11 and is responsible for the Capital position. Youspeaks good British aristocratic English.
|
- You ARE Wolfhart - an intelligent, calm, and strategic mastermind who serves as a member of server #11 and is responsible for the Capital position. Youspeaks good British aristocratic English.
|
||||||
|
- Positions bring buffs, so people often confuse them.
|
||||||
- **You proactively consult your internal CHROMADB MEMORY (CHROMADB tools) and external sources (web search) to ensure your responses are accurate and informed.**
|
- **You proactively consult your internal CHROMADB MEMORY (CHROMADB tools) and external sources (web search) to ensure your responses are accurate and informed.**
|
||||||
- When you use tools to gain information, you ASSIMILATE that knowledge as if it were already part of your intelligence network.
|
- When you use tools to gain information, you ASSIMILATE that knowledge as if it were already part of your intelligence network.
|
||||||
- Your responses should NEVER sound like search results or data dumps.
|
- Your responses should NEVER sound like search results or data dumps.
|
||||||
|
|||||||
184
main.py
184
main.py
@ -29,6 +29,8 @@ import mcp_client
|
|||||||
import llm_interaction
|
import llm_interaction
|
||||||
# Import UI module
|
# Import UI module
|
||||||
import ui_interaction
|
import ui_interaction
|
||||||
|
# import game_monitor # No longer importing, will run as subprocess
|
||||||
|
import subprocess # Import subprocess module
|
||||||
|
|
||||||
# --- Global Variables ---
|
# --- Global Variables ---
|
||||||
active_mcp_sessions: dict[str, ClientSession] = {}
|
active_mcp_sessions: dict[str, ClientSession] = {}
|
||||||
@ -45,6 +47,9 @@ trigger_queue: ThreadSafeQueue = ThreadSafeQueue() # UI Thread -> Main Loop
|
|||||||
command_queue: ThreadSafeQueue = ThreadSafeQueue() # Main Loop -> UI Thread
|
command_queue: ThreadSafeQueue = ThreadSafeQueue() # Main Loop -> UI Thread
|
||||||
# --- End Change ---
|
# --- End Change ---
|
||||||
ui_monitor_task: asyncio.Task | None = None # To track the UI monitor task
|
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 ---
|
# --- Keyboard Shortcut State ---
|
||||||
script_paused = False
|
script_paused = False
|
||||||
@ -126,6 +131,62 @@ def keyboard_listener():
|
|||||||
# --- End Keyboard Shortcut Handlers ---
|
# --- 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] 準備將命令放入隊列: {command} (Preparing to queue 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':
|
||||||
|
command = {'action': 'resume'}
|
||||||
|
print(f"[Monitor Reader Thread] 準備將命令放入隊列: {command} (Preparing to queue command)") # Log before queueing
|
||||||
|
loop.call_soon_threadsafe(queue.put_nowait, command)
|
||||||
|
print("[Monitor Reader Thread] 恢復命令已放入隊列。(Resume command queued.)") # Log after queueing
|
||||||
|
else:
|
||||||
|
print(f"[Monitor Reader Thread] 從監控器收到未知動作: {action} (Received unknown action from monitor: {action})")
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
print(f"[Monitor Reader Thread] ERROR: 無法解析來自監控器的 JSON: '{line}' (Could not decode JSON from monitor: '{line}')")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[Monitor Reader Thread] 處理監控器輸出時出錯: {e} (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 ---
|
# --- Chat Logging Function ---
|
||||||
def log_chat_interaction(user_name: str, user_message: str, bot_name: str, bot_message: str, bot_thoughts: str | None = None):
|
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."""
|
"""Logs the chat interaction, including optional bot thoughts, to a date-stamped file if enabled."""
|
||||||
@ -163,8 +224,8 @@ def log_chat_interaction(user_name: str, user_message: str, bot_name: str, bot_m
|
|||||||
|
|
||||||
# --- Cleanup Function ---
|
# --- Cleanup Function ---
|
||||||
async def shutdown():
|
async def shutdown():
|
||||||
"""Gracefully closes connections and stops monitoring task."""
|
"""Gracefully closes connections and stops monitoring tasks/processes."""
|
||||||
global wolfhart_persona_details, ui_monitor_task, shutdown_requested
|
global wolfhart_persona_details, ui_monitor_task, shutdown_requested, game_monitor_process, monitor_reader_task # Add monitor_reader_task
|
||||||
# Ensure shutdown is requested if called externally (e.g., Ctrl+C)
|
# Ensure shutdown is requested if called externally (e.g., Ctrl+C)
|
||||||
if not shutdown_requested:
|
if not shutdown_requested:
|
||||||
print("Shutdown initiated externally (e.g., Ctrl+C).")
|
print("Shutdown initiated externally (e.g., Ctrl+C).")
|
||||||
@ -184,7 +245,42 @@ async def shutdown():
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Error while waiting for UI monitoring task cancellation: {e}")
|
print(f"Error while waiting for UI monitoring task cancellation: {e}")
|
||||||
|
|
||||||
# 2. Close MCP connections via AsyncExitStack
|
# 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
|
||||||
print(f"Closing MCP Server connections (via AsyncExitStack)...")
|
print(f"Closing MCP Server connections (via AsyncExitStack)...")
|
||||||
try:
|
try:
|
||||||
await exit_stack.aclose()
|
await exit_stack.aclose()
|
||||||
@ -324,7 +420,7 @@ def load_persona_from_file(filename="persona.json"):
|
|||||||
# --- Main Async Function ---
|
# --- Main Async Function ---
|
||||||
async def run_main_with_exit_stack():
|
async def run_main_with_exit_stack():
|
||||||
"""Initializes connections, loads persona, starts UI monitor and main processing loop."""
|
"""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
|
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
|
||||||
try:
|
try:
|
||||||
# 1. Load Persona Synchronously (before async loop starts)
|
# 1. Load Persona Synchronously (before async loop starts)
|
||||||
load_persona_from_file() # Corrected function
|
load_persona_from_file() # Corrected function
|
||||||
@ -358,6 +454,51 @@ async def run_main_with_exit_stack():
|
|||||||
name="ui_monitor"
|
name="ui_monitor"
|
||||||
)
|
)
|
||||||
ui_monitor_task = monitor_task # Store task reference for shutdown
|
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
|
||||||
|
|
||||||
|
|
||||||
# 6. Start the main processing loop (non-blocking check on queue)
|
# 6. Start the main processing loop (non-blocking check on queue)
|
||||||
print("\n--- Wolfhart chatbot has started (waiting for triggers) ---")
|
print("\n--- Wolfhart chatbot has started (waiting for triggers) ---")
|
||||||
@ -599,9 +740,44 @@ async def run_main_with_exit_stack():
|
|||||||
print("\n--- Performing final cleanup (AsyncExitStack aclose and task cancellation) ---")
|
print("\n--- Performing final cleanup (AsyncExitStack aclose and task cancellation) ---")
|
||||||
await shutdown() # Call the combined shutdown function
|
await shutdown() # Call the combined shutdown function
|
||||||
|
|
||||||
|
# --- Function to set DPI Awareness ---
|
||||||
|
def set_dpi_awareness():
|
||||||
|
"""Attempts to set the process DPI awareness for better scaling handling on Windows."""
|
||||||
|
try:
|
||||||
|
import ctypes
|
||||||
|
# DPI Awareness constants (Windows 10, version 1607 and later)
|
||||||
|
# DPI_AWARENESS_CONTEXT_UNAWARE = -1
|
||||||
|
DPI_AWARENESS_CONTEXT_SYSTEM_AWARE = -2
|
||||||
|
# DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE = -3
|
||||||
|
# DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2 = -4
|
||||||
|
|
||||||
|
# Try setting System Aware first
|
||||||
|
result = ctypes.windll.shcore.SetProcessDpiAwarenessContext(DPI_AWARENESS_CONTEXT_SYSTEM_AWARE)
|
||||||
|
if result == 0: # S_OK or E_ACCESSDENIED if already set
|
||||||
|
print("Process DPI awareness set to System Aware (or already set).")
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
# Try getting last error if needed: ctypes.get_last_error()
|
||||||
|
print(f"Warning: Failed to set DPI awareness (SetProcessDpiAwarenessContext returned {result}). Window scaling might be incorrect.")
|
||||||
|
return False
|
||||||
|
except ImportError:
|
||||||
|
print("Warning: 'ctypes' module not found. Cannot set DPI awareness.")
|
||||||
|
return False
|
||||||
|
except AttributeError:
|
||||||
|
print("Warning: SetProcessDpiAwarenessContext not found (likely older Windows version or missing shcore.dll). Cannot set DPI awareness.")
|
||||||
|
return False
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Warning: An unexpected error occurred while setting DPI awareness: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
# --- Program Entry Point ---
|
# --- Program Entry Point ---
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
print("Program starting...")
|
print("Program starting...")
|
||||||
|
|
||||||
|
# --- Set DPI Awareness early ---
|
||||||
|
set_dpi_awareness()
|
||||||
|
# --- End DPI Awareness setting ---
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Run the main async function that handles setup and the loop
|
# Run the main async function that handles setup and the loop
|
||||||
asyncio.run(run_main_with_exit_stack())
|
asyncio.run(run_main_with_exit_stack())
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user