diff --git a/readme.md b/readme.md index 9a51e51..ee1499e 100644 --- a/readme.md +++ b/readme.md @@ -60,4 +60,11 @@ - 서버 메모리 크기는 `ansible/roles/fabric_server/defaults/main.yml`의 `server_xms`, `server_xmx` 변수로 조정할 수 있습니다. - 자세한 과정은 [Notion 문서](https://www.notion.so/MC-2241afe72e6980da8b2ac86e0bcf270e)를 참고하실 수 있습니다. +### Mods 관리 GUI +`scripts/mod_manager.py`를 실행하면 간단한 창이 열리며 Modrinth에서 모드를 검색하여 목록에 추가하거나 삭제할 수 있습니다. 실행 예시는 다음과 같습니다. +```bash +python3 scripts/mod_manager.py +``` +새 모드를 추가할 때는 슬러그를 입력하면 현재 Minecraft 버전에 맞는 최신 파일을 자동으로 다운로드해 `ansible/vars/mods.yml`에 등록합니다. + diff --git a/scripts/mod_manager.py b/scripts/mod_manager.py new file mode 100644 index 0000000..6ffe252 --- /dev/null +++ b/scripts/mod_manager.py @@ -0,0 +1,125 @@ +#!/usr/bin/env python3 +"""Simple GUI to manage Minecraft mods list using Modrinth API.""" + +from __future__ import annotations + +import hashlib +import os +import tkinter as tk +from tkinter import messagebox, simpledialog +from typing import Dict + +import requests +import yaml + +# File paths relative to this script +REPO_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +MODS_FILE = os.path.join(REPO_ROOT, "ansible", "vars", "mods.yml") +VERSIONS_FILE = os.path.join(REPO_ROOT, "ansible", "vars", "versions.yml") + + +def load_yaml(path: str) -> Dict: + with open(path, "r", encoding="utf-8") as f: + return yaml.safe_load(f) or {} + + +def save_yaml(path: str, data: Dict) -> None: + with open(path, "w", encoding="utf-8") as f: + yaml.safe_dump(data, f, allow_unicode=True, sort_keys=False) + + +def get_mc_version() -> str: + versions = load_yaml(VERSIONS_FILE) + return versions.get("afabric_mc_version", "") + + +def fetch_modrinth_latest(slug: str, mc_version: str) -> Dict[str, str]: + params = { + "loaders": ["fabric"], + "game_versions": [mc_version], + "limit": 1, + } + resp = requests.get( + f"https://api.modrinth.com/v2/project/{slug}/version", + params=params, + timeout=10, + ) + resp.raise_for_status() + versions = resp.json() + if not versions: + raise RuntimeError("No compatible version found") + file_info = versions[0]["files"][0] + url = file_info["url"] + filename = file_info["filename"] + data = requests.get(url, timeout=10).content + checksum = hashlib.sha256(data).hexdigest() + return {"name": filename, "url": url, "checksum": f"sha256:{checksum}"} + + +def refresh_list(listbox: tk.Listbox, mods: Dict) -> None: + listbox.delete(0, tk.END) + for mod in mods.get("fabric_mods", []): + listbox.insert(tk.END, mod["name"]) + + +def add_mod(root: tk.Tk, listbox: tk.Listbox, mods: Dict) -> None: + slug = simpledialog.askstring( + "Add Mod", + "Modrinth slug or project ID:", + parent=root, + ) + if not slug: + return + try: + mc_ver = get_mc_version() + entry = fetch_modrinth_latest(slug, mc_ver) + except Exception as exc: # noqa: BLE001 + messagebox.showerror("Error", str(exc), parent=root) + return + mods.setdefault("fabric_mods", []).append(entry) + save_yaml(MODS_FILE, mods) + refresh_list(listbox, mods) + + +def remove_mod(root: tk.Tk, listbox: tk.Listbox, mods: Dict) -> None: + sel = listbox.curselection() + if not sel: + return + idx = sel[0] + confirm = messagebox.askyesno( + "Remove", + f"Delete {mods['fabric_mods'][idx]['name']}?", + parent=root, + ) + if confirm: + mods["fabric_mods"].pop(idx) + save_yaml(MODS_FILE, mods) + refresh_list(listbox, mods) + + +def main() -> None: + mods = load_yaml(MODS_FILE) + root = tk.Tk() + root.title("Minecraft Mod Manager") + listbox = tk.Listbox(root, width=80) + listbox.pack(fill=tk.BOTH, expand=True) + refresh_list(listbox, mods) + + btn_frame = tk.Frame(root) + btn_frame.pack(fill=tk.X) + tk.Button( + btn_frame, + text="Add", + command=lambda: add_mod(root, listbox, mods), + ).pack(side=tk.LEFT) + tk.Button( + btn_frame, + text="Remove", + command=lambda: remove_mod(root, listbox, mods), + ).pack(side=tk.LEFT) + + root.mainloop() + + +if __name__ == "__main__": + main()