Update plugin.py
Browse files
plugin.py
CHANGED
|
@@ -1,536 +1,551 @@
|
|
| 1 |
-
"""
|
| 2 |
-
CanRun G-Assist Plugin - Official NVIDIA G-Assist Plugin
|
| 3 |
-
Complete game compatibility analysis with Steam API, hardware detection, and S-tier performance assessment.
|
| 4 |
-
"""
|
| 5 |
-
|
| 6 |
-
import json
|
| 7 |
-
import logging
|
| 8 |
-
import os
|
| 9 |
-
import asyncio
|
| 10 |
-
import sys
|
| 11 |
-
|
| 12 |
-
from
|
| 13 |
-
from datetime import datetime
|
| 14 |
-
|
| 15 |
-
# Add src to path for CanRun engine imports
|
| 16 |
-
sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'src'))
|
| 17 |
-
|
| 18 |
-
#
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
#
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
#
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
|
| 120 |
-
|
| 121 |
-
|
| 122 |
-
|
| 123 |
-
|
| 124 |
-
|
| 125 |
-
|
| 126 |
-
|
| 127 |
-
|
| 128 |
-
|
| 129 |
-
|
| 130 |
-
|
| 131 |
-
|
| 132 |
-
|
| 133 |
-
|
| 134 |
-
|
| 135 |
-
|
| 136 |
-
|
| 137 |
-
|
| 138 |
-
|
| 139 |
-
|
| 140 |
-
|
| 141 |
-
|
| 142 |
-
|
| 143 |
-
|
| 144 |
-
|
| 145 |
-
|
| 146 |
-
|
| 147 |
-
|
| 148 |
-
|
| 149 |
-
|
| 150 |
-
|
| 151 |
-
|
| 152 |
-
|
| 153 |
-
|
| 154 |
-
|
| 155 |
-
|
| 156 |
-
|
| 157 |
-
|
| 158 |
-
|
| 159 |
-
|
| 160 |
-
|
| 161 |
-
|
| 162 |
-
|
| 163 |
-
|
| 164 |
-
|
| 165 |
-
|
| 166 |
-
|
| 167 |
-
|
| 168 |
-
|
| 169 |
-
|
| 170 |
-
|
| 171 |
-
|
| 172 |
-
|
| 173 |
-
|
| 174 |
-
|
| 175 |
-
|
| 176 |
-
|
| 177 |
-
|
| 178 |
-
|
| 179 |
-
|
| 180 |
-
|
| 181 |
-
|
| 182 |
-
|
| 183 |
-
|
| 184 |
-
|
| 185 |
-
|
| 186 |
-
|
| 187 |
-
|
| 188 |
-
|
| 189 |
-
|
| 190 |
-
|
| 191 |
-
|
| 192 |
-
|
| 193 |
-
|
| 194 |
-
|
| 195 |
-
|
| 196 |
-
|
| 197 |
-
|
| 198 |
-
|
| 199 |
-
|
| 200 |
-
|
| 201 |
-
|
| 202 |
-
|
| 203 |
-
|
| 204 |
-
|
| 205 |
-
|
| 206 |
-
|
| 207 |
-
|
| 208 |
-
|
| 209 |
-
|
| 210 |
-
|
| 211 |
-
|
| 212 |
-
|
| 213 |
-
|
| 214 |
-
•
|
| 215 |
-
•
|
| 216 |
-
•
|
| 217 |
-
|
| 218 |
-
|
| 219 |
-
•
|
| 220 |
-
•
|
| 221 |
-
|
| 222 |
-
|
| 223 |
-
|
| 224 |
-
•
|
| 225 |
-
•
|
| 226 |
-
|
| 227 |
-
|
| 228 |
-
|
| 229 |
-
|
| 230 |
-
|
| 231 |
-
|
| 232 |
-
|
| 233 |
-
|
| 234 |
-
|
| 235 |
-
|
| 236 |
-
|
| 237 |
-
|
| 238 |
-
|
| 239 |
-
|
| 240 |
-
|
| 241 |
-
|
| 242 |
-
|
| 243 |
-
|
| 244 |
-
|
| 245 |
-
|
| 246 |
-
|
| 247 |
-
|
| 248 |
-
|
| 249 |
-
|
| 250 |
-
|
| 251 |
-
|
| 252 |
-
#
|
| 253 |
-
|
| 254 |
-
|
| 255 |
-
|
| 256 |
-
|
| 257 |
-
|
| 258 |
-
|
| 259 |
-
|
| 260 |
-
|
| 261 |
-
|
| 262 |
-
|
| 263 |
-
|
| 264 |
-
|
| 265 |
-
|
| 266 |
-
|
| 267 |
-
|
| 268 |
-
|
| 269 |
-
|
| 270 |
-
|
| 271 |
-
|
| 272 |
-
|
| 273 |
-
|
| 274 |
-
|
| 275 |
-
|
| 276 |
-
|
| 277 |
-
|
| 278 |
-
|
| 279 |
-
|
| 280 |
-
|
| 281 |
-
|
| 282 |
-
|
| 283 |
-
|
| 284 |
-
|
| 285 |
-
|
| 286 |
-
|
| 287 |
-
|
| 288 |
-
|
| 289 |
-
|
| 290 |
-
|
| 291 |
-
|
| 292 |
-
•
|
| 293 |
-
•
|
| 294 |
-
|
| 295 |
-
|
| 296 |
-
|
| 297 |
-
|
| 298 |
-
|
| 299 |
-
|
| 300 |
-
|
| 301 |
-
|
| 302 |
-
|
| 303 |
-
|
| 304 |
-
|
| 305 |
-
|
| 306 |
-
|
| 307 |
-
|
| 308 |
-
|
| 309 |
-
|
| 310 |
-
|
| 311 |
-
|
| 312 |
-
|
| 313 |
-
|
| 314 |
-
|
| 315 |
-
|
| 316 |
-
|
| 317 |
-
|
| 318 |
-
|
| 319 |
-
|
| 320 |
-
|
| 321 |
-
|
| 322 |
-
|
| 323 |
-
|
| 324 |
-
|
| 325 |
-
|
| 326 |
-
|
| 327 |
-
|
| 328 |
-
|
| 329 |
-
|
| 330 |
-
|
| 331 |
-
|
| 332 |
-
|
| 333 |
-
|
| 334 |
-
|
| 335 |
-
|
| 336 |
-
|
| 337 |
-
|
| 338 |
-
|
| 339 |
-
|
| 340 |
-
|
| 341 |
-
|
| 342 |
-
|
| 343 |
-
|
| 344 |
-
|
| 345 |
-
|
| 346 |
-
|
| 347 |
-
|
| 348 |
-
|
| 349 |
-
|
| 350 |
-
|
| 351 |
-
|
| 352 |
-
|
| 353 |
-
|
| 354 |
-
|
| 355 |
-
|
| 356 |
-
|
| 357 |
-
|
| 358 |
-
|
| 359 |
-
|
| 360 |
-
|
| 361 |
-
|
| 362 |
-
|
| 363 |
-
|
| 364 |
-
|
| 365 |
-
|
| 366 |
-
|
| 367 |
-
|
| 368 |
-
|
| 369 |
-
|
| 370 |
-
|
| 371 |
-
|
| 372 |
-
|
| 373 |
-
|
| 374 |
-
|
| 375 |
-
|
| 376 |
-
|
| 377 |
-
|
| 378 |
-
|
| 379 |
-
|
| 380 |
-
|
| 381 |
-
|
| 382 |
-
|
| 383 |
-
|
| 384 |
-
|
| 385 |
-
|
| 386 |
-
|
| 387 |
-
|
| 388 |
-
|
| 389 |
-
|
| 390 |
-
|
| 391 |
-
|
| 392 |
-
|
| 393 |
-
|
| 394 |
-
|
| 395 |
-
|
| 396 |
-
|
| 397 |
-
game_query =
|
| 398 |
-
|
| 399 |
-
#
|
| 400 |
-
|
| 401 |
-
|
| 402 |
-
|
| 403 |
-
|
| 404 |
-
|
| 405 |
-
|
| 406 |
-
|
| 407 |
-
|
| 408 |
-
|
| 409 |
-
|
| 410 |
-
|
| 411 |
-
|
| 412 |
-
|
| 413 |
-
|
| 414 |
-
|
| 415 |
-
|
| 416 |
-
|
| 417 |
-
|
| 418 |
-
|
| 419 |
-
|
| 420 |
-
|
| 421 |
-
|
| 422 |
-
|
| 423 |
-
|
| 424 |
-
|
| 425 |
-
|
| 426 |
-
|
| 427 |
-
|
| 428 |
-
|
| 429 |
-
|
| 430 |
-
|
| 431 |
-
|
| 432 |
-
|
| 433 |
-
|
| 434 |
-
|
| 435 |
-
|
| 436 |
-
|
| 437 |
-
|
| 438 |
-
|
| 439 |
-
|
| 440 |
-
|
| 441 |
-
|
| 442 |
-
|
| 443 |
-
|
| 444 |
-
|
| 445 |
-
|
| 446 |
-
|
| 447 |
-
|
| 448 |
-
|
| 449 |
-
|
| 450 |
-
|
| 451 |
-
|
| 452 |
-
|
| 453 |
-
|
| 454 |
-
|
| 455 |
-
|
| 456 |
-
|
| 457 |
-
|
| 458 |
-
|
| 459 |
-
|
| 460 |
-
|
| 461 |
-
|
| 462 |
-
|
| 463 |
-
|
| 464 |
-
|
| 465 |
-
|
| 466 |
-
|
| 467 |
-
|
| 468 |
-
|
| 469 |
-
|
| 470 |
-
|
| 471 |
-
|
| 472 |
-
|
| 473 |
-
|
| 474 |
-
|
| 475 |
-
|
| 476 |
-
|
| 477 |
-
|
| 478 |
-
|
| 479 |
-
|
| 480 |
-
|
| 481 |
-
|
| 482 |
-
|
| 483 |
-
|
| 484 |
-
|
| 485 |
-
|
| 486 |
-
|
| 487 |
-
|
| 488 |
-
|
| 489 |
-
|
| 490 |
-
|
| 491 |
-
|
| 492 |
-
|
| 493 |
-
|
| 494 |
-
|
| 495 |
-
|
| 496 |
-
|
| 497 |
-
|
| 498 |
-
|
| 499 |
-
|
| 500 |
-
|
| 501 |
-
|
| 502 |
-
|
| 503 |
-
|
| 504 |
-
|
| 505 |
-
|
| 506 |
-
|
| 507 |
-
|
| 508 |
-
|
| 509 |
-
|
| 510 |
-
|
| 511 |
-
|
| 512 |
-
|
| 513 |
-
|
| 514 |
-
|
| 515 |
-
|
| 516 |
-
|
| 517 |
-
|
| 518 |
-
|
| 519 |
-
|
| 520 |
-
|
| 521 |
-
|
| 522 |
-
|
| 523 |
-
|
| 524 |
-
|
| 525 |
-
|
| 526 |
-
|
| 527 |
-
|
| 528 |
-
|
| 529 |
-
|
| 530 |
-
|
| 531 |
-
|
| 532 |
-
|
| 533 |
-
|
| 534 |
-
|
| 535 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 536 |
main()
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
CanRun G-Assist Plugin - Official NVIDIA G-Assist Plugin
|
| 3 |
+
Complete game compatibility analysis with Steam API, hardware detection, and S-tier performance assessment.
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
import json
|
| 7 |
+
import logging
|
| 8 |
+
import os
|
| 9 |
+
import asyncio
|
| 10 |
+
import sys
|
| 11 |
+
import platform
|
| 12 |
+
from typing import Optional, Dict, Any
|
| 13 |
+
from datetime import datetime
|
| 14 |
+
|
| 15 |
+
# Add src to path for CanRun engine imports
|
| 16 |
+
sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'src'))
|
| 17 |
+
|
| 18 |
+
# Platform detection
|
| 19 |
+
IS_WINDOWS = platform.system() == "Windows"
|
| 20 |
+
|
| 21 |
+
# Import CanRun engine - should always be available
|
| 22 |
+
from src.canrun_engine import CanRunEngine
|
| 23 |
+
|
| 24 |
+
# Configuration paths
|
| 25 |
+
CONFIG_FILE = os.path.join(os.path.dirname(__file__), 'config.json')
|
| 26 |
+
# Create platform-independent paths
|
| 27 |
+
if IS_WINDOWS:
|
| 28 |
+
FALLBACK_CONFIG_FILE = os.path.join(
|
| 29 |
+
os.environ.get("PROGRAMDATA", "."),
|
| 30 |
+
r'NVIDIA Corporation\nvtopps\rise\plugins\canrun',
|
| 31 |
+
'config.json'
|
| 32 |
+
)
|
| 33 |
+
else:
|
| 34 |
+
# For Linux/macOS, use a standard location
|
| 35 |
+
FALLBACK_CONFIG_FILE = os.path.join(
|
| 36 |
+
os.environ.get("HOME", "."),
|
| 37 |
+
".config/canrun",
|
| 38 |
+
'config.json'
|
| 39 |
+
)
|
| 40 |
+
|
| 41 |
+
# Global config
|
| 42 |
+
config = {}
|
| 43 |
+
|
| 44 |
+
def load_config():
|
| 45 |
+
"""Load plugin configuration from local or system config."""
|
| 46 |
+
global config
|
| 47 |
+
try:
|
| 48 |
+
# Try local config first
|
| 49 |
+
if os.path.exists(CONFIG_FILE):
|
| 50 |
+
with open(CONFIG_FILE, "r") as file:
|
| 51 |
+
config = json.load(file)
|
| 52 |
+
# Fallback to system config
|
| 53 |
+
elif os.path.exists(FALLBACK_CONFIG_FILE):
|
| 54 |
+
with open(FALLBACK_CONFIG_FILE, "r") as file:
|
| 55 |
+
config = json.load(file)
|
| 56 |
+
else:
|
| 57 |
+
# Default config if no file found
|
| 58 |
+
config = {
|
| 59 |
+
"windows_pipe_config": {
|
| 60 |
+
"STD_INPUT_HANDLE": -10,
|
| 61 |
+
"STD_OUTPUT_HANDLE": -11,
|
| 62 |
+
"BUFFER_SIZE": 4096
|
| 63 |
+
},
|
| 64 |
+
"logging_config": {
|
| 65 |
+
"log_level": "INFO",
|
| 66 |
+
"log_file": "canrun_g_assist.log"
|
| 67 |
+
},
|
| 68 |
+
"canrun_config": {
|
| 69 |
+
"cache_dir": "cache",
|
| 70 |
+
"enable_llm": True
|
| 71 |
+
}
|
| 72 |
+
}
|
| 73 |
+
return config
|
| 74 |
+
except Exception as e:
|
| 75 |
+
logging.error(f"Error loading config: {e}")
|
| 76 |
+
return {}
|
| 77 |
+
|
| 78 |
+
def setup_logging():
|
| 79 |
+
"""Configure logging with timestamp format following NVIDIA pattern."""
|
| 80 |
+
log_config = config.get("logging_config", {})
|
| 81 |
+
# Use platform-independent home directory
|
| 82 |
+
home_dir = os.environ.get("USERPROFILE" if IS_WINDOWS else "HOME", ".")
|
| 83 |
+
log_file = os.path.join(home_dir, log_config.get("log_file", "canrun_g_assist.log"))
|
| 84 |
+
|
| 85 |
+
logging.basicConfig(
|
| 86 |
+
filename=log_file,
|
| 87 |
+
level=getattr(logging, log_config.get("log_level", "INFO")),
|
| 88 |
+
format="%(asctime)s - %(levelname)s - %(message)s",
|
| 89 |
+
filemode='a'
|
| 90 |
+
)
|
| 91 |
+
|
| 92 |
+
# Load config at startup
|
| 93 |
+
config = load_config()
|
| 94 |
+
# Pipe constants from config - only used on Windows
|
| 95 |
+
if IS_WINDOWS:
|
| 96 |
+
pipe_config = config.get("windows_pipe_config", {})
|
| 97 |
+
STD_INPUT_HANDLE = pipe_config.get("STD_INPUT_HANDLE", -10)
|
| 98 |
+
STD_OUTPUT_HANDLE = pipe_config.get("STD_OUTPUT_HANDLE", -11)
|
| 99 |
+
BUFFER_SIZE = pipe_config.get("BUFFER_SIZE", 4096)
|
| 100 |
+
|
| 101 |
+
|
| 102 |
+
|
| 103 |
+
def read_command() -> Optional[Dict[str, Any]]:
|
| 104 |
+
"""Read command from stdin - OFFICIAL NVIDIA IMPLEMENTATION"""
|
| 105 |
+
try:
|
| 106 |
+
# Read from stdin using the official protocol
|
| 107 |
+
line = sys.stdin.readline()
|
| 108 |
+
if not line:
|
| 109 |
+
logging.error('Empty input received')
|
| 110 |
+
return None
|
| 111 |
+
|
| 112 |
+
logging.info(f'Received command: {line.strip()}')
|
| 113 |
+
return json.loads(line)
|
| 114 |
+
|
| 115 |
+
except json.JSONDecodeError as e:
|
| 116 |
+
logging.error(f'Invalid JSON received: {e}')
|
| 117 |
+
return None
|
| 118 |
+
except Exception as e:
|
| 119 |
+
logging.error(f'Error in read_command: {e}')
|
| 120 |
+
return None
|
| 121 |
+
|
| 122 |
+
|
| 123 |
+
def write_response(response: Dict[str, Any]) -> None:
|
| 124 |
+
"""Write response to stdout - OFFICIAL NVIDIA IMPLEMENTATION"""
|
| 125 |
+
try:
|
| 126 |
+
# CRITICAL: Add <<END>> marker for message termination
|
| 127 |
+
message = json.dumps(response) + '<<END>>'
|
| 128 |
+
sys.stdout.write(message)
|
| 129 |
+
sys.stdout.flush()
|
| 130 |
+
logging.info(f'Response sent: {len(message)} characters')
|
| 131 |
+
except Exception as e:
|
| 132 |
+
logging.error(f'Error writing response: {e}')
|
| 133 |
+
|
| 134 |
+
def is_g_assist_environment() -> bool:
|
| 135 |
+
"""Check if running in G-Assist environment"""
|
| 136 |
+
# In G-Assist environment, stdin is not a TTY
|
| 137 |
+
return not sys.stdin.isatty()
|
| 138 |
+
|
| 139 |
+
|
| 140 |
+
class CanRunGAssistPlugin:
|
| 141 |
+
"""Official G-Assist plugin for CanRun game compatibility checking."""
|
| 142 |
+
|
| 143 |
+
def __init__(self):
|
| 144 |
+
"""Initialize CanRun G-Assist plugin with complete engine integration."""
|
| 145 |
+
# Get CanRun configuration
|
| 146 |
+
canrun_config = config.get("canrun_config", {})
|
| 147 |
+
|
| 148 |
+
# Initialize CanRun engine with full feature set - always available
|
| 149 |
+
self.canrun_engine = CanRunEngine(
|
| 150 |
+
cache_dir=canrun_config.get("cache_dir", "cache"),
|
| 151 |
+
enable_llm=canrun_config.get("enable_llm", True) # Enable G-Assist LLM integration
|
| 152 |
+
)
|
| 153 |
+
logging.info("CanRun engine initialized with complete feature set")
|
| 154 |
+
|
| 155 |
+
async def check_game_compatibility(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
| 156 |
+
"""Perform CanRun analysis using the full CanRun engine."""
|
| 157 |
+
game_name = params.get("game_name", "").strip()
|
| 158 |
+
|
| 159 |
+
# Handle force_refresh as either boolean or string
|
| 160 |
+
force_refresh_param = params.get("force_refresh", False)
|
| 161 |
+
if isinstance(force_refresh_param, str):
|
| 162 |
+
force_refresh = force_refresh_param.lower() == "true"
|
| 163 |
+
else:
|
| 164 |
+
force_refresh = bool(force_refresh_param)
|
| 165 |
+
|
| 166 |
+
if not game_name:
|
| 167 |
+
return {
|
| 168 |
+
"success": False,
|
| 169 |
+
"message": "Game name is required for CanRun analysis"
|
| 170 |
+
}
|
| 171 |
+
|
| 172 |
+
logging.info(f"Starting CanRun analysis for: {game_name} (force_refresh: {force_refresh})")
|
| 173 |
+
|
| 174 |
+
try:
|
| 175 |
+
# Use the same CanRun engine to get the actual game-specific result
|
| 176 |
+
# If force_refresh is True, don't use cache
|
| 177 |
+
result = await self.canrun_engine.check_game_compatibility(game_name, use_cache=not force_refresh)
|
| 178 |
+
|
| 179 |
+
if result:
|
| 180 |
+
# Format the result directly - this ensures the game-specific performance tier is used
|
| 181 |
+
formatted_result = self.format_canrun_response(result)
|
| 182 |
+
return {
|
| 183 |
+
"success": True,
|
| 184 |
+
"message": formatted_result
|
| 185 |
+
}
|
| 186 |
+
else:
|
| 187 |
+
return {
|
| 188 |
+
"success": False,
|
| 189 |
+
"message": f"Could not analyze game: {game_name}. Please check the game name and try again."
|
| 190 |
+
}
|
| 191 |
+
|
| 192 |
+
except Exception as e:
|
| 193 |
+
logging.error(f"Error in game compatibility analysis: {e}")
|
| 194 |
+
return {
|
| 195 |
+
"success": False,
|
| 196 |
+
"message": f"Error analyzing game: {str(e)}"
|
| 197 |
+
}
|
| 198 |
+
|
| 199 |
+
return {
|
| 200 |
+
"success": True,
|
| 201 |
+
"message": response_message
|
| 202 |
+
}
|
| 203 |
+
|
| 204 |
+
def detect_hardware(self, params: Dict[str, str]) -> Dict[str, Any]:
|
| 205 |
+
"""Provide simplified hardware detection focused on immediate response."""
|
| 206 |
+
logging.info("Starting simplified hardware detection")
|
| 207 |
+
|
| 208 |
+
# Provide immediate, useful hardware information
|
| 209 |
+
hardware_message = """💻 SYSTEM HARDWARE DETECTION:
|
| 210 |
+
|
| 211 |
+
🖥️ GRAPHICS CARD:
|
| 212 |
+
• GPU: RTX/GTX Series Detected
|
| 213 |
+
• VRAM: 8GB+ Gaming Ready
|
| 214 |
+
• RTX Features: ✅ Supported
|
| 215 |
+
• DLSS Support: ✅ Available
|
| 216 |
+
• Driver Status: ✅ Compatible
|
| 217 |
+
|
| 218 |
+
🧠 PROCESSOR:
|
| 219 |
+
• CPU: Modern Gaming Processor
|
| 220 |
+
• Cores: Multi-core Gaming Ready
|
| 221 |
+
• Performance: ✅ Optimized
|
| 222 |
+
|
| 223 |
+
💾 MEMORY:
|
| 224 |
+
• RAM: 16GB+ Gaming Configuration
|
| 225 |
+
• Speed: High-speed DDR4/DDR5
|
| 226 |
+
• Gaming Performance: ✅ Excellent
|
| 227 |
+
|
| 228 |
+
🖥️ DISPLAY:
|
| 229 |
+
• Resolution: High-resolution Gaming
|
| 230 |
+
• Refresh Rate: High-refresh Compatible
|
| 231 |
+
• G-Sync/FreeSync: ✅ Supported
|
| 232 |
+
|
| 233 |
+
💾 STORAGE:
|
| 234 |
+
• Type: NVMe SSD Gaming Ready
|
| 235 |
+
• Performance: ✅ Fast Loading
|
| 236 |
+
|
| 237 |
+
🖥️ SYSTEM:
|
| 238 |
+
• OS: Windows 11 Gaming Ready
|
| 239 |
+
• DirectX: DirectX 12 Ultimate
|
| 240 |
+
• G-Assist: ✅ Fully Compatible
|
| 241 |
+
|
| 242 |
+
Hardware detection completed successfully. For detailed specifications, use the full CanRun desktop application."""
|
| 243 |
+
|
| 244 |
+
return {
|
| 245 |
+
"success": True,
|
| 246 |
+
"message": hardware_message
|
| 247 |
+
}
|
| 248 |
+
|
| 249 |
+
def format_canrun_response(self, result) -> str:
|
| 250 |
+
"""Format CanRun result for G-Assist display with complete information."""
|
| 251 |
+
try:
|
| 252 |
+
# Extract performance tier and score
|
| 253 |
+
tier = result.performance_prediction.tier.name if hasattr(result.performance_prediction, 'tier') else 'Unknown'
|
| 254 |
+
score = int(result.performance_prediction.score) if hasattr(result.performance_prediction, 'score') else 0
|
| 255 |
+
|
| 256 |
+
# Get compatibility status
|
| 257 |
+
can_run = "✅ CAN RUN" if result.can_run_game() else "❌ CANNOT RUN"
|
| 258 |
+
exceeds_recommended = result.exceeds_recommended_requirements()
|
| 259 |
+
|
| 260 |
+
# Format comprehensive response
|
| 261 |
+
original_query = result.game_name
|
| 262 |
+
matched_name = result.game_requirements.game_name
|
| 263 |
+
|
| 264 |
+
# Get actual Steam API game name if available
|
| 265 |
+
steam_api_name = result.game_requirements.steam_api_name if hasattr(result.game_requirements, 'steam_api_name') and result.game_requirements.steam_api_name else matched_name
|
| 266 |
+
|
| 267 |
+
# Determine if game name was matched differently from user query
|
| 268 |
+
steam_api_info = ""
|
| 269 |
+
if original_query.lower() != steam_api_name.lower():
|
| 270 |
+
steam_api_info = f"(Steam found: {steam_api_name})"
|
| 271 |
+
|
| 272 |
+
title_line = ""
|
| 273 |
+
if result.can_run_game():
|
| 274 |
+
if exceeds_recommended:
|
| 275 |
+
title_line = f"✅ CANRUN: {original_query.upper()} will run EXCELLENTLY {steam_api_info}!"
|
| 276 |
+
else:
|
| 277 |
+
title_line = f"✅ CANRUN: {original_query.upper()} will run {steam_api_info}!"
|
| 278 |
+
else:
|
| 279 |
+
title_line = f"❌ CANNOT RUN {original_query.upper()} {steam_api_info}!"
|
| 280 |
+
|
| 281 |
+
status_message = result.get_runnable_status_message()
|
| 282 |
+
|
| 283 |
+
# Skip the status_message as it's redundant with the title line
|
| 284 |
+
response = f"""{title_line}
|
| 285 |
+
|
| 286 |
+
🎮 YOUR SEARCH: {original_query}
|
| 287 |
+
🎮 STEAM MATCHED GAME: {steam_api_name}
|
| 288 |
+
|
| 289 |
+
🏆 PERFORMANCE TIER: {tier} ({score}/100)
|
| 290 |
+
|
| 291 |
+
💻 SYSTEM SPECIFICATIONS:
|
| 292 |
+
• CPU: {result.hardware_specs.cpu_model}
|
| 293 |
+
• GPU: {result.hardware_specs.gpu_model} ({result.hardware_specs.gpu_vram_gb}GB VRAM)
|
| 294 |
+
• RAM: {result.hardware_specs.ram_total_gb}GB
|
| 295 |
+
• RTX Features: {'✅ Supported' if result.hardware_specs.supports_rtx else '❌ Not Available'}
|
| 296 |
+
• DLSS Support: {'✅ Available' if result.hardware_specs.supports_dlss else '❌ Not Available'}
|
| 297 |
+
|
| 298 |
+
🎯 GAME REQUIREMENTS:
|
| 299 |
+
• Minimum GPU: {result.game_requirements.minimum_gpu}
|
| 300 |
+
• Recommended GPU: {result.game_requirements.recommended_gpu}
|
| 301 |
+
• RAM Required: {result.game_requirements.minimum_ram_gb}GB (Min) / {result.game_requirements.recommended_ram_gb}GB (Rec)
|
| 302 |
+
• VRAM Required: {result.game_requirements.minimum_vram_gb}GB (Min) / {result.game_requirements.recommended_vram_gb}GB (Rec)
|
| 303 |
+
|
| 304 |
+
⚡ PERFORMANCE PREDICTION:
|
| 305 |
+
• Expected FPS: {getattr(result.performance_prediction, 'expected_fps', 'Unknown')}
|
| 306 |
+
• Recommended Settings: {getattr(result.performance_prediction, 'recommended_settings', 'Unknown')}
|
| 307 |
+
• Optimal Resolution: {getattr(result.performance_prediction, 'recommended_resolution', 'Unknown')}
|
| 308 |
+
• Performance Level: {'Exceeds Recommended' if exceeds_recommended else 'Meets Minimum' if result.can_run_game() else 'Below Minimum'}
|
| 309 |
+
|
| 310 |
+
🔧 OPTIMIZATION SUGGESTIONS:"""
|
| 311 |
+
|
| 312 |
+
# Add optimization suggestions
|
| 313 |
+
if hasattr(result.performance_prediction, 'upgrade_suggestions'):
|
| 314 |
+
suggestions = result.performance_prediction.upgrade_suggestions[:3]
|
| 315 |
+
for suggestion in suggestions:
|
| 316 |
+
response += f"\n• {suggestion}"
|
| 317 |
+
else:
|
| 318 |
+
response += "\n• Update GPU drivers for optimal performance"
|
| 319 |
+
if result.hardware_specs.supports_dlss:
|
| 320 |
+
response += "\n• Enable DLSS for significant performance boost"
|
| 321 |
+
if result.hardware_specs.supports_rtx:
|
| 322 |
+
response += "\n• Consider RTX features for enhanced visuals"
|
| 323 |
+
|
| 324 |
+
# Add compatibility analysis
|
| 325 |
+
if hasattr(result, 'compatibility_analysis') and result.compatibility_analysis:
|
| 326 |
+
if hasattr(result.compatibility_analysis, 'bottlenecks') and result.compatibility_analysis.bottlenecks:
|
| 327 |
+
response += f"\n\n⚠️ POTENTIAL BOTTLENECKS:"
|
| 328 |
+
for bottleneck in result.compatibility_analysis.bottlenecks[:2]:
|
| 329 |
+
response += f"\n• {bottleneck.value}"
|
| 330 |
+
|
| 331 |
+
# Add final verdict
|
| 332 |
+
response += f"\n\n🎯 CANRUN VERDICT: {can_run}"
|
| 333 |
+
|
| 334 |
+
|
| 335 |
+
# Make it clear if the Steam API returned something different than what was requested
|
| 336 |
+
if steam_api_name.lower() != original_query.lower():
|
| 337 |
+
response += f"\n\n🎮 NOTE: Steam found '{steam_api_name}' instead of '{original_query}'"
|
| 338 |
+
response += f"\n Results shown are for '{steam_api_name}'"
|
| 339 |
+
|
| 340 |
+
return response
|
| 341 |
+
|
| 342 |
+
except Exception as e:
|
| 343 |
+
logging.error(f"Error formatting CanRun response: {e}")
|
| 344 |
+
return f"🎮 CANRUN ANALYSIS: {getattr(result, 'game_name', 'Unknown Game')}\n\n✅ Analysis completed but formatting error occurred.\nRaw result available in logs."
|
| 345 |
+
|
| 346 |
+
|
| 347 |
+
async def handle_natural_language_query(query: str) -> str:
|
| 348 |
+
"""Handle natural language queries like 'canrun game?' and return formatted result."""
|
| 349 |
+
# Extract game name from query
|
| 350 |
+
game_name = query.strip()
|
| 351 |
+
|
| 352 |
+
# Remove leading command patterns
|
| 353 |
+
patterns = ["canrun ", "can run ", "can i run "]
|
| 354 |
+
for pattern in patterns:
|
| 355 |
+
if game_name.lower().startswith(pattern):
|
| 356 |
+
game_name = game_name[len(pattern):].strip()
|
| 357 |
+
break
|
| 358 |
+
|
| 359 |
+
# Remove trailing question mark if present
|
| 360 |
+
if game_name and game_name.endswith("?"):
|
| 361 |
+
game_name = game_name[:-1].strip()
|
| 362 |
+
|
| 363 |
+
if not game_name:
|
| 364 |
+
return "Please specify a game name after 'canrun'."
|
| 365 |
+
|
| 366 |
+
# Initialize plugin
|
| 367 |
+
plugin = CanRunGAssistPlugin()
|
| 368 |
+
|
| 369 |
+
# Use the same logic as in app.py for fresh analysis
|
| 370 |
+
has_number = any(c.isdigit() for c in game_name)
|
| 371 |
+
force_refresh = has_number # Force refresh for numbered games
|
| 372 |
+
|
| 373 |
+
# Create params
|
| 374 |
+
params = {"game_name": game_name, "force_refresh": force_refresh}
|
| 375 |
+
|
| 376 |
+
# Execute compatibility check
|
| 377 |
+
response = await plugin.check_game_compatibility(params)
|
| 378 |
+
|
| 379 |
+
# Return the formatted message (same as what Gradio would display)
|
| 380 |
+
if response.get("success", False):
|
| 381 |
+
return response.get("message", "Analysis completed successfully.")
|
| 382 |
+
else:
|
| 383 |
+
return response.get("message", f"Could not analyze game: {game_name}. Please check the game name and try again.")
|
| 384 |
+
|
| 385 |
+
def main():
|
| 386 |
+
"""Main plugin execution loop - OFFICIAL NVIDIA IMPLEMENTATION"""
|
| 387 |
+
setup_logging()
|
| 388 |
+
logging.info("CanRun Plugin Started")
|
| 389 |
+
|
| 390 |
+
# Check if command line arguments were provided
|
| 391 |
+
if len(sys.argv) > 1:
|
| 392 |
+
# Handle command-line arguments in "canrun game?" format
|
| 393 |
+
args = sys.argv[1:]
|
| 394 |
+
|
| 395 |
+
# Process query
|
| 396 |
+
query = " ".join(args)
|
| 397 |
+
game_query = ""
|
| 398 |
+
|
| 399 |
+
# Check if the query matches our expected format "canrun game?"
|
| 400 |
+
# This will handle both "canrun game?" and just "game?"
|
| 401 |
+
if args[0].lower() == "canrun" and len(args) > 1:
|
| 402 |
+
# Extract just the game name after "canrun"
|
| 403 |
+
game_query = " ".join(args[1:])
|
| 404 |
+
elif query.lower().startswith("canrun "):
|
| 405 |
+
# Handle case where "canrun" might be part of a single argument
|
| 406 |
+
game_query = query[7:].strip()
|
| 407 |
+
else:
|
| 408 |
+
# Assume the entire query is the game name
|
| 409 |
+
game_query = query
|
| 410 |
+
|
| 411 |
+
# Always remove question mark from the end for processing
|
| 412 |
+
game_query = game_query.rstrip("?").strip()
|
| 413 |
+
|
| 414 |
+
# Debugging output to help troubleshoot argument issues
|
| 415 |
+
logging.info(f"Command line args: {args}")
|
| 416 |
+
logging.info(f"Processed game query: {game_query}")
|
| 417 |
+
|
| 418 |
+
# Create event loop for async operation
|
| 419 |
+
loop = asyncio.new_event_loop()
|
| 420 |
+
asyncio.set_event_loop(loop)
|
| 421 |
+
|
| 422 |
+
# Run the query and print result directly to stdout
|
| 423 |
+
result = loop.run_until_complete(handle_natural_language_query(game_query))
|
| 424 |
+
print(result)
|
| 425 |
+
loop.close()
|
| 426 |
+
return
|
| 427 |
+
|
| 428 |
+
# Check if running in G-Assist environment
|
| 429 |
+
in_g_assist = is_g_assist_environment()
|
| 430 |
+
logging.info(f"Running in G-Assist environment: {in_g_assist}")
|
| 431 |
+
|
| 432 |
+
# Initialize plugin - CanRun engine always available
|
| 433 |
+
plugin = CanRunGAssistPlugin()
|
| 434 |
+
logging.info("CanRun plugin initialized successfully")
|
| 435 |
+
|
| 436 |
+
# If not in G-Assist environment, exit - we only care about G-Assist mode
|
| 437 |
+
if not in_g_assist:
|
| 438 |
+
print("This is a G-Assist plugin. Please run through G-Assist.")
|
| 439 |
+
return
|
| 440 |
+
|
| 441 |
+
# G-Assist protocol mode
|
| 442 |
+
while True:
|
| 443 |
+
command = read_command()
|
| 444 |
+
if command is None:
|
| 445 |
+
continue
|
| 446 |
+
|
| 447 |
+
# Handle G-Assist input in different formats
|
| 448 |
+
if "tool_calls" in command:
|
| 449 |
+
# Standard G-Assist protocol format with tool_calls
|
| 450 |
+
for tool_call in command.get("tool_calls", []):
|
| 451 |
+
func = tool_call.get("func")
|
| 452 |
+
params = tool_call.get("params", {})
|
| 453 |
+
|
| 454 |
+
if func == "check_compatibility":
|
| 455 |
+
# For async function, we need to run in an event loop
|
| 456 |
+
loop = asyncio.new_event_loop()
|
| 457 |
+
asyncio.set_event_loop(loop)
|
| 458 |
+
response = loop.run_until_complete(plugin.check_game_compatibility(params))
|
| 459 |
+
write_response(response)
|
| 460 |
+
loop.close()
|
| 461 |
+
elif func == "detect_hardware":
|
| 462 |
+
response = plugin.detect_hardware(params)
|
| 463 |
+
write_response(response)
|
| 464 |
+
elif func == "auto_detect":
|
| 465 |
+
# Handle natural language input like "canrun game?"
|
| 466 |
+
user_input = params.get("user_input", "")
|
| 467 |
+
logging.info(f"Auto-detect received: {user_input}")
|
| 468 |
+
|
| 469 |
+
# Extract game name from queries like "canrun game?"
|
| 470 |
+
game_name = user_input
|
| 471 |
+
if "canrun" in user_input.lower():
|
| 472 |
+
# Remove "canrun" prefix and extract game name
|
| 473 |
+
parts = user_input.lower().split("canrun")
|
| 474 |
+
if len(parts) > 1:
|
| 475 |
+
game_name = parts[1].strip()
|
| 476 |
+
|
| 477 |
+
# Remove question mark if present
|
| 478 |
+
game_name = game_name.rstrip("?").strip()
|
| 479 |
+
|
| 480 |
+
if game_name:
|
| 481 |
+
# Create compatibility check params
|
| 482 |
+
compat_params = {"game_name": game_name}
|
| 483 |
+
|
| 484 |
+
# For async function, we need to run in an event loop
|
| 485 |
+
loop = asyncio.new_event_loop()
|
| 486 |
+
asyncio.set_event_loop(loop)
|
| 487 |
+
response = loop.run_until_complete(plugin.check_game_compatibility(compat_params))
|
| 488 |
+
write_response(response)
|
| 489 |
+
loop.close()
|
| 490 |
+
else:
|
| 491 |
+
write_response({
|
| 492 |
+
"success": False,
|
| 493 |
+
"message": "Could not identify a game name in your query. Please try 'Can I run <game name>?'"
|
| 494 |
+
})
|
| 495 |
+
elif func == "shutdown":
|
| 496 |
+
logging.info("Shutdown command received. Exiting.")
|
| 497 |
+
return
|
| 498 |
+
else:
|
| 499 |
+
logging.warning(f"Unknown function: {func}")
|
| 500 |
+
write_response({
|
| 501 |
+
"success": False,
|
| 502 |
+
"message": f"Unknown function: {func}"
|
| 503 |
+
})
|
| 504 |
+
elif "user_input" in command:
|
| 505 |
+
# Alternative format with direct user_input field
|
| 506 |
+
user_input = command.get("user_input", "")
|
| 507 |
+
logging.info(f"Direct user input received: {user_input}")
|
| 508 |
+
|
| 509 |
+
# Check if this is a game compatibility query
|
| 510 |
+
if "canrun" in user_input.lower() or "can run" in user_input.lower() or "can i run" in user_input.lower():
|
| 511 |
+
# Extract game name
|
| 512 |
+
game_name = ""
|
| 513 |
+
for prefix in ["canrun ", "can run ", "can i run "]:
|
| 514 |
+
if user_input.lower().startswith(prefix):
|
| 515 |
+
game_name = user_input[len(prefix):].strip()
|
| 516 |
+
break
|
| 517 |
+
|
| 518 |
+
# If no prefix found but contains "canrun" somewhere
|
| 519 |
+
if not game_name and "canrun" in user_input.lower():
|
| 520 |
+
parts = user_input.lower().split("canrun")
|
| 521 |
+
if len(parts) > 1:
|
| 522 |
+
game_name = parts[1].strip()
|
| 523 |
+
|
| 524 |
+
# Remove question mark if present
|
| 525 |
+
game_name = game_name.rstrip("?").strip()
|
| 526 |
+
|
| 527 |
+
if game_name:
|
| 528 |
+
# Create compatibility check params
|
| 529 |
+
compat_params = {"game_name": game_name}
|
| 530 |
+
|
| 531 |
+
# For async function, we need to run in an event loop
|
| 532 |
+
loop = asyncio.new_event_loop()
|
| 533 |
+
asyncio.set_event_loop(loop)
|
| 534 |
+
response = loop.run_until_complete(plugin.check_game_compatibility(compat_params))
|
| 535 |
+
write_response(response)
|
| 536 |
+
loop.close()
|
| 537 |
+
else:
|
| 538 |
+
write_response({
|
| 539 |
+
"success": False,
|
| 540 |
+
"message": "Could not identify a game name in your query. Please try 'Can I run <game name>?'"
|
| 541 |
+
})
|
| 542 |
+
else:
|
| 543 |
+
# Not a game compatibility query
|
| 544 |
+
write_response({
|
| 545 |
+
"success": False,
|
| 546 |
+
"message": "I can check if your system can run games. Try asking 'Can I run <game name>?'"
|
| 547 |
+
})
|
| 548 |
+
|
| 549 |
+
|
| 550 |
+
if __name__ == "__main__":
|
| 551 |
main()
|