Files
airdef/tools/airdef.py
T
chexit a6a13003c2
build / mod (push) Failing after 5m36s
build / game-linux (push) Has been skipped
build / game-macos (push) Has been cancelled
build / game-windows (push) Has been cancelled
build / release (push) Has been cancelled
AirDef: drones and air defence for a Horizon fork
The mod (airdef-mod/), its generated models and sounds, the fork's patches
over upstream Horizon (horizon-patches/, HORIZON_COMMIT), a cross-platform
build/install/run script (tools/airdef.py), CI that builds and releases on
every push (.gitea/workflows/build.yml) and a setup guide (docs/INSTALL.md).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 19:16:52 +03:00

154 lines
6.2 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
"""AirDef: сборка, установка и запуск на Windows, macOS и Linux. Только стандартная библиотека Python.
python3 tools/airdef.py fork # скачать Horizon нужной версии и наложить патчи форка (horizon/)
python3 tools/airdef.py game # собрать игру (форк) — нужен Rust
python3 tools/airdef.py mod # собрать мод airdef.hmod — нужен Rust с wasm32
python3 tools/airdef.py install # положить мод в папку игры и включить
python3 tools/airdef.py play [аргументы игры] # запустить форк на папке игры
python3 tools/airdef.py all # всё по порядку: fork, game, mod, install
Переменные окружения: HORIZON_HOME (папка игры), HORIZON_RESOURCES (подготовленные ресурсы
Minecraft 26.3), AIRDEF_GAME (готовый horizon-client вместо собранного).
"""
import glob
import os
import platform
import shutil
import subprocess
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parent.parent
FORK = ROOT / "horizon"
UPSTREAM = "https://github.com/ludovic111/horizon.git"
EXE = ".exe" if os.name == "nt" else ""
def data_dir() -> Path:
"""Как `dirs::data_dir()` в Horizon."""
system = platform.system()
if system == "Windows":
return Path(os.environ["APPDATA"])
if system == "Darwin":
return Path.home() / "Library" / "Application Support"
return Path(os.environ.get("XDG_DATA_HOME", Path.home() / ".local" / "share"))
def game_home() -> Path:
return Path(os.environ.get("HORIZON_HOME") or data_dir() / "horizon")
def run(cmd, cwd=None, env=None):
print("+", " ".join(str(c) for c in cmd), flush=True)
subprocess.run([str(c) for c in cmd], cwd=cwd, env=env, check=True)
def cargo() -> str:
found = shutil.which("cargo") or str(Path.home() / ".cargo" / "bin" / ("cargo" + EXE))
if not Path(found).exists() and not shutil.which("cargo"):
sys.exit("Нужен Rust: https://rustup.rs (затем: rustup target add wasm32-unknown-unknown)")
return found
def fork():
"""horizon/ = апстрим на коммите HORIZON_COMMIT + патчи horizon-patches/."""
commit = (ROOT / "HORIZON_COMMIT").read_text().strip()
patches = sorted((ROOT / "horizon-patches").glob("*.patch"))
if not (FORK / ".git").exists():
run(["git", "clone", "--filter=blob:none", UPSTREAM, FORK])
branch = subprocess.run(["git", "-C", FORK, "branch", "--show-current"], capture_output=True, text=True).stdout.strip()
if branch == "airdef":
print("horizon/: ветка airdef уже есть")
return
run(["git", "-C", FORK, "fetch", "--quiet", "origin", commit])
run(["git", "-C", FORK, "checkout", "-B", "airdef", commit])
if patches:
run(["git", "-C", FORK, "-c", "user.name=airdef", "-c", "user.email=airdef@localhost",
"am", "--3way", *patches])
def game():
if not (FORK / "Cargo.toml").exists():
fork()
run([cargo(), "build", "--profile", "fast", "-p", "horizon-client", "--features", "mods"], cwd=FORK)
def built_game() -> Path:
if os.environ.get("AIRDEF_GAME"):
return Path(os.environ["AIRDEF_GAME"])
for name in (FORK / "target" / "fast" / ("horizon-client" + EXE), ROOT / ("horizon-client" + EXE)):
if name.exists():
return name
sys.exit("Нет собранной игры: python3 tools/airdef.py game (или AIRDEF_GAME=путь к horizon-client)")
def mod():
tool = shutil.which("cargo-horizon-mod") or Path.home() / ".cargo" / "bin" / ("cargo-horizon-mod" + EXE)
if not Path(tool).exists():
if not (FORK / "Cargo.toml").exists():
fork()
run([cargo(), "install", "--locked", "--path", FORK / "crates" / "cargo-horizon-mod"])
run([cargo(), "horizon-mod", "build"], cwd=ROOT / "airdef-mod")
def hmod() -> Path:
for p in (ROOT / "airdef-mod" / "target" / "horizon-mod" / "airdef.hmod", ROOT / "airdef.hmod"):
if p.exists():
return p
sys.exit("Нет airdef.hmod: python3 tools/airdef.py mod")
def install():
home = game_home()
mods = home / "horizon-mods"
mods.mkdir(parents=True, exist_ok=True)
shutil.rmtree(mods / "airdef", ignore_errors=True) # первая версия мода была папкой
shutil.copy2(hmod(), mods / "airdef.hmod")
listing = home / "horizon-mods.txt"
lines = listing.read_text().splitlines() if listing.exists() else []
lines = [l for l in lines if not l.startswith("mod.airdef:")] + ["mod.airdef:true"]
listing.write_text("\n".join(lines) + "\n")
print(f"Мод установлен в {mods} и включён")
def resources() -> Path:
if os.environ.get("HORIZON_RESOURCES"):
return Path(os.environ["HORIZON_RESOURCES"])
found = sorted(glob.glob(str(data_dir() / "horizon-launcher" / "versions" / "*" / ".local" / "horizon-client" / "26.3")))
local = FORK / ".local" / "horizon-client" / "26.3"
if local.exists():
found.append(str(local))
if not found:
sys.exit("Не найдены ресурсы игры. Установи Horizon через лаунчер и запусти его один раз "
"(он подготовит ресурсы из твоего Minecraft 26.3), либо задай HORIZON_RESOURCES.")
return Path(found[-1])
def play(args):
exe = built_game().resolve()
home = game_home()
home.mkdir(parents=True, exist_ok=True)
# Игра хранит настройки, моды и миры в текущей папке.
run([exe, "--resources", resources(), *args], cwd=home)
def main():
if len(sys.argv) < 2:
sys.exit(__doc__)
cmd, rest = sys.argv[1], sys.argv[2:]
steps = {"fork": fork, "game": game, "mod": mod, "install": install}
if cmd == "play":
play(rest)
elif cmd == "all":
for step in ("fork", "game", "mod", "install"):
steps[step]()
elif cmd in steps:
steps[cmd]()
else:
sys.exit(__doc__)
if __name__ == "__main__":
main()