Начинающий
- Статус
- Оффлайн
- Регистрация
- 15 Май 2024
- Сообщения
- 78
- Реакции
- 6
Крч увидел Hytale захотелось поиграть подумал покупать нахуй нужно качать лаунчеры в рот ебал ну и решил скриптик написать да очень простенько без онлайн фикса но если кому то интересно чисто затестить Hytale понять что это то вот
code:
import argparse
import hashlib
import json
import os
import shutil
import subprocess
import sys
import tarfile
import time
import uuid
import zipfile
from pathlib import Path
from urllib.request import Request, urlopen
from urllib.error import HTTPError, URLError
USER_AGENT = "HytaleTT/1.0"
def get_appdata_dir() -> Path:
appdata = os.environ.get("APPDATA")
if appdata:
return Path(appdata)
return Path.home() / "AppData" / "Roaming"
def http_head(url: str):
req = Request(url, method="HEAD", headers={"User-Agent": USER_AGENT})
try:
with urlopen(req, timeout=15) as resp:
size = resp.headers.get("Content-Length")
return True, int(size) if size else None
except HTTPError as e:
if e.code == 404:
return False, None
raise
def http_get_text(url: str) -> str:
req = Request(url, headers={"User-Agent": USER_AGENT})
with urlopen(req, timeout=30) as resp:
return resp.read().decode("utf-8")
def download_file(url: str, dest: Path, expected_size=None, expected_sha256=None):
dest.parent.mkdir(parents=True, exist_ok=True)
tmp_path = dest.with_suffix(dest.suffix + ".tmp")
req = Request(url, headers={"User-Agent": USER_AGENT})
with urlopen(req, timeout=60) as resp:
total = expected_size
if total is None:
length = resp.headers.get("Content-Length")
total = int(length) if length else 0
downloaded = 0
start_time = time.time()
with open(tmp_path, "wb") as f:
while True:
chunk = resp.read(1024 * 1024)
if not chunk:
break
f.write(chunk)
downloaded += len(chunk)
if total > 0:
pct = downloaded / total * 100
elapsed = max(time.time() - start_time, 0.001)
speed = downloaded / elapsed / (1024 * 1024)
print(f"\rDownloading... {pct:5.1f}% ({speed:.1f} MB/s)", end="")
if total > 0:
print()
if expected_size is not None and expected_size > 0:
actual = tmp_path.stat().st_size
if actual != expected_size:
tmp_path.unlink(missing_ok=True)
raise RuntimeError(f"Size mismatch: expected {expected_size}, got {actual}")
if expected_sha256:
actual_hash = compute_sha256(tmp_path)
if actual_hash.lower() != expected_sha256.lower():
tmp_path.unlink(missing_ok=True)
raise RuntimeError("SHA256 mismatch for downloaded file")
tmp_path.replace(dest)
def compute_sha256(path: Path) -> str:
h = hashlib.sha256()
with open(path, "rb") as f:
for chunk in iter(lambda: f.read(1024 * 1024), b""):
h.update(chunk)
return h.hexdigest()
def extract_archive(archive_path: Path, dest_dir: Path):
dest_dir.mkdir(parents=True, exist_ok=True)
if zipfile.is_zipfile(archive_path):
with zipfile.ZipFile(archive_path) as zf:
zf.extractall(dest_dir)
return
if tarfile.is_tarfile(archive_path):
with tarfile.open(archive_path, "r:*") as tf:
tf.extractall(dest_dir)
return
raise RuntimeError("Unsupported archive format")
def flatten_directory(dir_path: Path):
subdirs = [p for p in dir_path.iterdir() if p.is_dir()]
if len(subdirs) == 1:
subdir = subdirs[0]
for item in subdir.iterdir():
target = dir_path / item.name
if target.exists():
if target.is_dir():
shutil.rmtree(target)
else:
target.unlink()
shutil.move(str(item), str(target))
shutil.rmtree(subdir)
def find_butler_exe(butler_dir: Path) -> Path:
direct = butler_dir / "butler.exe"
if direct.exists():
return direct
for path in butler_dir.rglob("butler.exe"):
# Move to root for simpler invocation
target = butler_dir / "butler.exe"
if path != target:
shutil.copy2(path, target)
return target
raise RuntimeError("butler.exe not found after extraction")
def ensure_butler(launcher_dir: Path) -> Path:
butler_dir = launcher_dir / "butler"
butler_dir.mkdir(parents=True, exist_ok=True)
butler_exe = butler_dir / "butler.exe"
if butler_exe.exists():
return butler_exe
cache_dir = launcher_dir / "cache"
cache_dir.mkdir(parents=True, exist_ok=True)
zip_path = cache_dir / "butler.zip"
url = "https://broth.itch.zone/butler/windows-amd64/LATEST/archive/default"
print("Downloading butler...")
download_file(url, zip_path)
print("Extracting butler...")
extract_archive(zip_path, butler_dir)
zip_path.unlink(missing_ok=True)
return find_butler_exe(butler_dir)
def apply_pwr(butler_exe: Path, pwr_path: Path, game_dir: Path, launcher_dir: Path):
staging_dir = game_dir / "staging-temp"
if staging_dir.exists():
shutil.rmtree(staging_dir, ignore_errors=True)
game_dir.mkdir(parents=True, exist_ok=True)
staging_dir.mkdir(parents=True, exist_ok=True)
args = [
str(butler_exe),
"apply",
"--staging-dir",
str(staging_dir),
str(pwr_path),
str(game_dir),
]
log_path = launcher_dir / "butler.log"
with open(log_path, "a", encoding="utf-8") as log:
log.write(f"\n[{time.strftime('%Y-%m-%d %H:%M:%S')}] Running: {' '.join(args)}\n")
log.write(f"PWR size: {pwr_path.stat().st_size} bytes\n")
log.write(f"Game dir exists: {game_dir.exists()}\n")
proc = subprocess.run(
args,
cwd=str(launcher_dir),
capture_output=True,
text=True,
timeout=600,
)
with open(log_path, "a", encoding="utf-8") as log:
log.write(f"Exit code: {proc.returncode}\n")
log.write("Stdout:\n" + (proc.stdout or "") + "\n")
log.write("Stderr:\n" + (proc.stderr or "") + "\n")
shutil.rmtree(staging_dir, ignore_errors=True)
if proc.returncode != 0:
msg = (proc.stderr or proc.stdout or "Unknown error").strip()
raise RuntimeError(f"Butler error: {msg}")
def find_latest_version(base_url: str, branch: str) -> int:
consecutive_misses = 0
ver = 1
max_ver = 0
while consecutive_misses < 5:
url = f"{base_url}/{branch}/0/{ver}.pwr"
try:
exists, _ = http_head(url)
except Exception:
exists = False
if exists:
max_ver = ver
consecutive_misses = 0
else:
consecutive_misses += 1
ver += 1
if max_ver == 0:
raise RuntimeError("No versions found")
return max_ver
def download_jre(branch: str, launcher_dir: Path, game_base_dir: Path):
jre_dir = game_base_dir / "install" / branch / "package" / "jre" / "latest"
java_exe = jre_dir / "bin" / "java.exe"
if java_exe.exists():
print("JRE already installed")
return
url = f"https://launcher.hytale.com/version/{branch}/jre.json"
print(f"Fetching JRE info: {url}")
data = json.loads(http_get_text(url))
download_url = data.get("download_url") or data.get("downloadUrl")
if not download_url:
raise RuntimeError("download_url not found in jre.json")
os_key = "windows"
arch_key = "amd64"
platform = download_url.get(os_key, {}).get(arch_key)
if not platform:
raise RuntimeError("JRE not available for windows/amd64")
jre_url = platform.get("url")
jre_sha = platform.get("sha256")
if not jre_url:
raise RuntimeError("JRE url missing")
cache_dir = launcher_dir / "cache"
cache_dir.mkdir(parents=True, exist_ok=True)
file_name = Path(jre_url).name
archive_path = cache_dir / file_name
print("Downloading JRE...")
download_file(jre_url, archive_path, expected_sha256=jre_sha)
print("Extracting JRE...")
extract_archive(archive_path, jre_dir)
flatten_directory(jre_dir)
archive_path.unlink(missing_ok=True)
def get_or_create_uuid(players_path: Path, player_name: str) -> str:
players = {}
if players_path.exists():
try:
players = json.loads(players_path.read_text(encoding="utf-8"))
except Exception:
players = {}
key = player_name.lower()
if key in players:
u = players[key]
return u.replace("-", "")
new_uuid = uuid.uuid4().hex
players[key] = new_uuid
players_path.parent.mkdir(parents=True, exist_ok=True)
players_path.write_text(json.dumps(players, indent=2), encoding="utf-8")
return new_uuid
def launch_game(branch: str, game_base_dir: Path, launcher_dir: Path, player_name: str):
game_dir = game_base_dir / "install" / branch / "package" / "game" / "latest"
client_path = game_dir / "Client" / "HytaleClient.exe"
if not client_path.exists():
raise RuntimeError("Game client not found")
java_exe = game_base_dir / "install" / branch / "package" / "jre" / "latest" / "bin" / "java.exe"
if not java_exe.exists():
java_exe = Path("java")
user_data = game_base_dir / "UserData"
user_data.mkdir(parents=True, exist_ok=True)
players_path = launcher_dir / "players.json"
player_uuid = get_or_create_uuid(players_path, player_name)
args = [
str(client_path),
"--app-dir",
str(game_dir),
"--java-exec",
str(java_exe),
"--user-dir",
str(user_data),
"--auth-mode",
"offline",
"--uuid",
player_uuid,
"--name",
player_name,
]
print("Launching game...")
subprocess.Popen(args, cwd=str(client_path.parent))
def main():
parser = argparse.ArgumentParser(description="Download and run Hytale (release) without the launcher")
parser.add_argument("--branch", default="release", help="Branch name (default: release)")
parser.add_argument("--name", default="Giddyk", help="Player name (default: Giddyk)")
parser.add_argument("--no-launch", action="store_true", help="Only download and install")
parser.add_argument("--force", action="store_true", help="Force re-download even if installed")
args = parser.parse_args()
if os.name != "nt":
print("This script is intended for Windows")
return 1
base_url = "https://game-patches.hytale.com/patches/windows/amd64"
branch = args.branch
appdata = get_appdata_dir()
launcher_dir = appdata / "HyTaLauncher"
game_base_dir = appdata / "Hytale"
cache_dir = launcher_dir / "cache"
launcher_dir.mkdir(parents=True, exist_ok=True)
cache_dir.mkdir(parents=True, exist_ok=True)
print("Finding latest version...")
latest_version = find_latest_version(base_url, branch)
print(f"Latest version: {latest_version}")
game_dir = game_base_dir / "install" / branch / "package" / "game" / "latest"
client_path = game_dir / "Client" / "HytaleClient.exe"
version_file = game_dir / ".version"
if not args.force and client_path.exists() and version_file.exists():
try:
installed_version = int(version_file.read_text().strip())
except Exception:
installed_version = 0
if installed_version == latest_version:
print("Game already installed at latest version")
else:
print(f"Installed version: {installed_version}, will update")
download_jre(branch, launcher_dir, game_base_dir)
pwr_url = f"{base_url}/{branch}/0/{latest_version}.pwr"
pwr_path = cache_dir / f"{branch}_0_{latest_version}.pwr"
if args.force or not pwr_path.exists():
print(f"Downloading full patch: {pwr_url}")
exists, size = http_head(pwr_url)
if not exists:
raise RuntimeError("PWR file not found for latest version")
download_file(pwr_url, pwr_path, expected_size=size)
else:
print("Using cached PWR file")
butler_exe = ensure_butler(launcher_dir)
print("Applying patch...")
apply_pwr(butler_exe, pwr_path, game_dir, launcher_dir)
version_file.parent.mkdir(parents=True, exist_ok=True)
version_file.write_text(str(latest_version), encoding="utf-8")
if not args.no_launch:
launch_game(branch, game_base_dir, launcher_dir, args.name)
print("Done")
return 0
if __name__ == "__main__":
raise SystemExit(main())

