diff --git a/VERSION b/VERSION index 9bbaee4..2e60e2a 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.0.12 \ No newline at end of file +0.0.13 \ No newline at end of file diff --git a/modpack/modrinth.index.json b/modpack/modrinth.index.json index 1db1a6d..78d375c 100644 --- a/modpack/modrinth.index.json +++ b/modpack/modrinth.index.json @@ -4,7 +4,7 @@ "type": "forge", "version": "1.20.1" }, - "versionId": "0.0.12", + "versionId": "0.0.13", "name": "minimal-client-modpack", "summary": "Minimal client modpack for connecting to the server" "files": [ diff --git a/sync_minimal.py b/sync_minimal.py new file mode 100755 index 0000000..efef798 --- /dev/null +++ b/sync_minimal.py @@ -0,0 +1,212 @@ +#!/usr/bin/env python3 +import os +import re +import shutil +import hashlib +import json +import urllib.request +from datetime import datetime + +REPO_DIR = "/tmp/mod_repo" +SERVER_MODS_FILE = "/tmp/server_mods.txt" +MINECRAFT_MODS_DIR = "/home/minecraft/mods" +TACZ_DIR = "/home/minecraft/tacz" +STATE_DIR = f"{REPO_DIR}/state" +MODPACK_DIR = f"{REPO_DIR}/modpack" +VERSION_FILE = f"{REPO_DIR}/VERSION" +TRUEMODS_DIR = f"{REPO_DIR}/truemods" +TRUEGUNPACKS_DIR = f"{REPO_DIR}/truegunpacks" +QQ_GROUP_ID = "814734369" +QQ_API_URL = "http://192.168.2.106:25570" + +CLIENT_ONLY_MODS = [ + "xaero", "ambient", "apple", "inventoryprofiles", + "chunky", "patchouli", "worldedit", "spark", "customskinloader", "gd656killicon" +] + +def log(msg): + ts = datetime.now().strftime("%Y-%m-%d %H:%M:%S") + print(f"[{ts}] {msg}") + +def get_mod_name(jar_path): + basename = os.path.basename(jar_path) + name = re.sub(r'-\d+.*\.jar$', '', basename) + name = re.sub(r'^\[.*?\]\s*', '', name) + return name + +def is_client_only(mod_name): + mod_lower = mod_name.lower() + for client_mod in CLIENT_ONLY_MODS: + if client_mod in mod_lower: + return True + return False + +def get_hash(path): + with open(path, 'rb') as f: + return hashlib.sha1(f.read()).hexdigest() + +def load_previous_state(): + prev_mods = [] + prev_excluded = [] + if os.path.exists(f"{STATE_DIR}/mods.json"): + with open(f"{STATE_DIR}/mods.json", 'r') as f: + prev_mods = [line.strip() for line in f if line.strip()] + if os.path.exists(f"{STATE_DIR}/excluded_mods.json"): + with open(f"{STATE_DIR}/excluded_mods.json", 'r') as f: + prev_excluded = json.load(f) + return prev_mods, prev_excluded + +def sync_minimal_pack(): + for d in [TRUEMODS_DIR, TRUEGUNPACKS_DIR]: + if os.path.exists(d): + shutil.rmtree(d) + os.makedirs(d) + + with open(SERVER_MODS_FILE, 'r') as f: + server_mods = [line.strip() for line in f if line.strip() and not line.startswith('disabled')] + + included, excluded = [], [] + + for mod in server_mods: + base_name = re.sub(r'-\d+.*$', '', mod) + jar_file = None + for f in os.listdir(MINECRAFT_MODS_DIR): + if f.startswith(base_name) and f.endswith('.jar'): + jar_file = os.path.join(MINECRAFT_MODS_DIR, f) + break + + if jar_file: + mod_name = get_mod_name(jar_file) + filename = os.path.basename(jar_file) + if is_client_only(mod_name): + excluded.append(filename) + else: + shutil.copy(jar_file, TRUEMODS_DIR) + included.append(filename) + + if os.path.exists(TACZ_DIR): + for f in os.listdir(TACZ_DIR): + if f.endswith('.zip'): + shutil.copy(os.path.join(TACZ_DIR, f), TRUEGUNPACKS_DIR) + + return sorted(included), sorted(excluded), sorted(os.listdir(TRUEGUNPACKS_DIR)) + +def create_index(version, mods, gunpacks): + lines = ['{', ' "formatVersion": 1,', ' "game": {', ' "type": "forge",', + ' "version": "1.20.1"', ' },', f' "versionId": "{version}",', + ' "name": "minimal-client-modpack",', + ' "summary": "Minimal client modpack for connecting to the server"', ' "files": ['] + + first = True + for mod in mods: + path = f"{TRUEMODS_DIR}/{mod}" + if os.path.exists(path): + h = get_hash(path) + if not first: lines.append(',') + first = False + lines.append(f' {{"path": "mods/{mod}", "hashes": {{"sha1": "{h}"}}, "env": {{"client": ["required"], "server": ["unsupported"]}}, "downloads": []}}') + + for gp in gunpacks: + path = f"{TRUEGUNPACKS_DIR}/{gp}" + if os.path.exists(path): + h = get_hash(path) + if not first: lines.append(',') + first = False + lines.append(f' {{"path": "tacz/{gp}", "hashes": {{"sha1": "{h}"}}, "env": {{"client": ["required"], "server": ["unsupported"]}}, "downloads": []}}') + + lines.extend([' ]', '}']) + os.makedirs(MODPACK_DIR, exist_ok=True) + with open(f"{MODPACK_DIR}/modrinth.index.json", 'w') as f: + f.write('\n'.join(lines)) + +def send_qq_message(message): + try: + url = f"{QQ_API_URL}/send_group_msg" + data = json.dumps({ + "group_id": int(QQ_GROUP_ID), + "message": message + }).encode('utf-8') + req = urllib.request.Request(url, data=data, headers={'Content-Type': 'application/json'}) + with urllib.request.urlopen(req, timeout=10) as response: + result = json.loads(response.read().decode('utf-8')) + if result.get('status') == 'ok': + log("QQ notification sent successfully") + else: + log(f"QQ notification failed: {result}") + except Exception as e: + log(f"Failed to send QQ message: {e}") + +def main(): + log("Starting minimal modpack synchronization") + os.makedirs(STATE_DIR, exist_ok=True) + + prev_mods, prev_excluded = load_previous_state() + included, excluded, gunpacks = sync_minimal_pack() + + current = open(VERSION_FILE).read().strip() if os.path.exists(VERSION_FILE) else "0.0.0" + parts = current.split('.') + new_version = f"{parts[0]}.{parts[1]}.{int(parts[2]) + 1}" + + mods_changed = set(included) != set(prev_mods) if prev_mods else True + + if mods_changed: + log(f"Mod changes detected: {len(included)} mods now") + + prev_set = set(prev_mods) if prev_mods else set() + curr_set = set(included) + added_mods = curr_set - prev_set + removed_mods = prev_set - curr_set + + change_summary = [] + if added_mods: + change_summary.append(f"新增模组 ({len(added_mods)}):\n" + "\n".join([f" + {m}" for m in list(added_mods)[:5]])) + if len(added_mods) > 5: + change_summary.append(f" ... 及其他 {len(added_mods) - 5} 个") + if removed_mods: + change_summary.append(f"移除模组 ({len(removed_mods)}):\n" + "\n".join([f" - {m}" for m in list(removed_mods)[:5]])) + if len(removed_mods) > 5: + change_summary.append(f" ... 及其他 {len(removed_mods) - 5} 个") + + qq_message = f"""📦 整合包更新公告 +━━━━━━━━━━━━━━━━━━━━━━ +版本: v{new_version} +模组数量: {len(included)} 个 (+{len(added_mods) if added_mods else 0} -{len(removed_mods) if removed_mods else 0}) +枪包数量: {len(gunpacks)} 个 +━━━━━━━━━━━━━━━━━━━━━━ +""" + if change_summary: + qq_message += "\n".join(change_summary) + "\n" + qq_message += """━━━━━━━━━━━━━━━━━━━━━━ +📥 下载地址: ssh://git@192.168.2.106:220/jianf/servermods.git +或联系管理员获取最新整合包""" + + send_qq_message(qq_message) + + create_index(new_version, included, gunpacks) + open(VERSION_FILE, 'w').write(new_version) + + with open(f"{STATE_DIR}/mods.json", 'w') as f: + f.write('\n'.join(included)) + with open(f"{STATE_DIR}/excluded_mods.json", 'w') as f: + json.dump(excluded, f) + with open(f"{STATE_DIR}/gunpacks.json", 'w') as f: + f.write('\n'.join(gunpacks)) + + log(f"Version: {new_version}") + log(f"Mods: {len(included)} included, {len(excluded)} excluded (client-only)") + log(f"Gunpacks: {len(gunpacks)}") + + log("Updating git repository") + os.system("cd /tmp/mod_repo && git add -A >/dev/null 2>&1") + result = os.popen("cd /tmp/mod_repo && git commit -m 'Update minimal modpack to version " + new_version + "' 2>&1").read() + if "nothing to commit" not in result: + log("Pushing to remote") + os.system("cd /tmp/mod_repo && git push origin main >/dev/null 2>&1 &") + log(f"Successfully pushed version {new_version}") + else: + log("No changes to commit") + + log("Synchronization completed") + +if __name__ == "__main__": + main()