Something went wrong. Try again.
Native control, inspection, UI automation and evidence capture for a running Witcher 3 process.
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293"""Standalone game paths and file audits; no combat-policy dependency."""import hashlibimport jsonimport osfrom pathlib import Path
ROOT = Path(__file__).resolve().parent.parentGLOBAL_CONFIG = Path(os.environ.get('XDG_CONFIG_HOME', Path.home()/'.config'))/'witcher3-devkit/local-config.json'CONFIG = Path(os.environ.get('W3DEV_CONFIG', GLOBAL_CONFIG if GLOBAL_CONFIG.exists() else ROOT/'local-config.json')).expanduser().resolve()
def load_paths(config_path=CONFIG, environ=None): """Resolve local installation paths without requiring the game or receipts.
Environment overrides configuration. Relative configured paths resolve against the configuration file, so a client's current directory cannot change them. An explicitly selected missing config is an error; a source-only clone needs no configuration for help/discovery. """ env = os.environ if environ is None else environ config_path = Path(config_path).expanduser().resolve() if config_path.exists(): config = json.loads(config_path.read_text()) if not isinstance(config, dict) or type(config.get('schema_version')) is not int or config['schema_version'] != 1: raise ValueError('Devkit config requires schema_version: 1') unknown = set(config) - {'schema_version', 'game_directory', 'documents_directory'} if unknown: raise ValueError('Unknown devkit config fields: ' + ', '.join(sorted(unknown))) else: if env.get('W3DEV_CONFIG'): raise FileNotFoundError('W3DEV_CONFIG does not exist: ' + str(config_path)) config = {}
def path(key, variable, default): value = env.get(variable, config.get(key, str(default))) if not isinstance(value, str) or not value.strip(): raise ValueError(key + ' must be a nonempty path string') result = Path(value).expanduser() return (config_path.parent / result).resolve() if not result.is_absolute() else result.resolve()
game = path('game_directory', 'W3DEV_GAME', Path.home() / '.local/share/Steam/steamapps/common/The Witcher 3') documents = path('documents_directory', 'W3DEV_DOCUMENTS', game.parent.parent / 'compatdata/292030/pfx/drive_c/users/steamuser/Documents/The Witcher 3') return game, documents
GAME, DOCS = load_paths()INSTALLATION_ID = hashlib.sha256(str(GAME.resolve()).encode()).hexdigest()[:24]STATE_ROOT = Path(os.environ.get('XDG_STATE_HOME', Path.home()/'.local/state'))/'witcher3-devkit'/INSTALLATION_IDCACHE_ROOT = Path(os.environ.get('XDG_CACHE_HOME', Path.home()/'.cache'))/'witcher3-devkit'RUNTIME_ROOT = (Path(os.environ['XDG_RUNTIME_DIR'])/'witcher3-devkit'/INSTALLATION_ID if os.environ.get('XDG_RUNTIME_DIR') else STATE_ROOT)
def state_directory(): """Never manufacture a second empty runtime for an installed game.""" if not (STATE_ROOT/'deployment.json').is_file(): raise FileNotFoundError('Installation state missing at '+str(STATE_ROOT)+ '; use migrate-state --from the existing closed-game checkout') metadata=STATE_ROOT/'installation.json' if not metadata.is_file() or json.loads(metadata.read_text())!=dict( schema_version=1,game_directory=str(GAME),documents_directory=str(DOCS)): raise RuntimeError('Installation identity/configuration mismatch; do not switch paths during a session') (STATE_ROOT/'artifacts').mkdir(exist_ok=True, mode=0o700) return STATE_ROOT
def tool_path(relative): """Downloaded tools may be shared; source and compiled outputs never fall back.""" local=ROOT/'research'/relative return local if local.exists() else CACHE_ROOT/relative
def digest(path): return hashlib.sha256(path.read_bytes()).hexdigest()
def files(root): return {p.relative_to(root).as_posix(): digest(p) for p in root.rglob('*') if p.is_file()}
def ensure_game_closed(proc_root=Path('/proc')): for p in proc_root.iterdir(): if not p.name.isdigit(): continue try: comm = (p / 'comm').read_text().strip().lower() argv = (p / 'cmdline').read_bytes().split(b'\0') except (OSError, ProcessLookupError): continue # Wine may expose the executable as "MainThread" in /proc/comm. executable = argv[0].decode(errors='replace').replace('\\', '/').rsplit('/', 1)[-1].lower() if argv else '' if comm in ('witcher3.exe', 'witcher3') or executable == 'witcher3.exe': raise RuntimeError('Close Witcher 3 before changing installed mod files')