import got, { Got } from "got"; import fs from "node:fs"; import fse from "fs-extra"; import { execPromise, fastdownload, version_compare, verifySHA1 } from "../utils/utils.js"; import { yauzl_promise } from "../utils/ziplib.js"; import { Config } from "../utils/config.js"; import { logger } from "../utils/logger.js"; interface IForge { data: { MOJMAPS: { server: string; }; MAPPINGS: { server: string; }; }; } interface IVersion { downloads: { server_mappings: { url: string; }; }; } interface IForgeFile { format: string; category: string; hash: string; _id: string; } interface IForgeBuild { branch: string; build: number; mcversion: string; modified: string; version: string; _id: string; files: IForgeFile[]; } export class Forge { minecraft: string; loaderVersion: string; got: Got; path: string; constructor(minecraft: string, loaderVersion: string, path: string) { this.minecraft = minecraft; this.loaderVersion = loaderVersion; this.path = path; const config = Config.getConfig(); this.got = got.extend({ headers: { "User-Agent": "DeEarthX" }, hooks: { init: [ (options) => { if (config.mirror.bmclapi) { options.prefixUrl = "https://bmclapi2.bangbang93.com"; } else { options.prefixUrl = "http://maven.minecraftforge.net/"; } } ] } }); } async setup() { await this.installer(); const config = Config.getConfig(); if (config.mirror.bmclapi) { await this.library(); } await this.install(); if (version_compare(this.minecraft, "1.18") === -1) { await this.wshell(); } } async library() { const _downlist: [string, string][] = []; const data = await fs.promises.readFile(`${this.path}/forge-${this.minecraft}-${this.loaderVersion}-installer.jar`); const zip = await yauzl_promise(data); for (const entry of zip) { if (entry.fileName === "version.json" || entry.fileName === "install_profile.json") { const entryData = await entry.ReadEntry; JSON.parse(entryData.toString()).libraries.forEach(async (e: any) => { const t = e.downloads.artifact.path; _downlist.push([`https://bmclapi2.bangbang93.com/maven/${t}`, `${this.path}/libraries/${t}`]); }); } if (entry.fileName === "install_profile.json") { const entryData = await entry.ReadEntry; const json = JSON.parse(entryData.toString()) as IForge; const vjson = await this.got.get(`version/${this.minecraft}/json`).json(); console.log(`${new URL(vjson.downloads.server_mappings.url).pathname}`); const mojpath = this.MTP(json.data.MOJMAPS.server); _downlist.push([`https://bmclapi2.bangbang93.com/${new URL(vjson.downloads.server_mappings.url).pathname.slice(1)}`, `${this.path}/libraries/${mojpath}`]); const mappingobj = json.data.MAPPINGS.server; const path = this.MTP(mappingobj.replace(":mappings@txt", "@zip")); _downlist.push([`https://bmclapi2.bangbang93.com/maven/${path}`, `${this.path}/libraries/${path}`]); } } const downlist = [...new Set(_downlist)]; await fastdownload(downlist); } async install() { const config = Config.getConfig(); const javaCmd = config.javaPath || 'java'; let cmd = `${javaCmd} -jar forge-${this.minecraft}-${this.loaderVersion}-installer.jar --installServer`; if (config.mirror.bmclapi) { cmd += ` --mirror https://bmclapi2.bangbang93.com/maven/`; } await execPromise(cmd, { cwd: this.path }).catch((e) => { logger.error(`Forge 安装失败: ${e}`); throw e; }); } async installer() { const config = Config.getConfig(); let url = `forge/download?mcversion=${this.minecraft}&version=${this.loaderVersion}&category=installer&format=jar`; let expectedHash: string | undefined; if (config.mirror?.bmclapi) { try { const forgeInfo = await this.got.get(`forge/minecraft/${this.minecraft}`).json(); const forgeVersion = forgeInfo.find(f => f.version === this.loaderVersion); if (forgeVersion) { const installerFile = forgeVersion.files.find(f => f.category === 'installer' && f.format === 'jar'); if (installerFile) { expectedHash = installerFile.hash; logger.debug(`获取到 Forge installer hash: ${expectedHash}`); } } } catch (error) { logger.warn(`获取 Forge hash 信息失败,将跳过 hash 验证`, error); } } else { url = `net/minecraftforge/forge/${this.minecraft}-${this.loaderVersion}/forge-${this.minecraft}-${this.loaderVersion}-installer.jar`; } const res = (await this.got.get(url)).rawBody; const filePath = `${this.path}/forge-${this.minecraft}-${this.loaderVersion}-installer.jar`; await fse.outputFile(filePath, res); if (expectedHash) { if (!verifySHA1(filePath, expectedHash)) { logger.warn(`Forge installer hash 验证失败,删除文件并重试`); fs.unlinkSync(filePath); const res2 = (await this.got.get(url)).rawBody; await fse.outputFile(filePath, res2); if (!verifySHA1(filePath, expectedHash)) { throw new Error(`Forge installer hash 验证失败,文件可能已损坏`); } } } } private async wshell() { const config = Config.getConfig(); const javaCmd = config.javaPath || 'java'; const cmd = `${javaCmd} -jar forge-${this.minecraft}-${this.loaderVersion}.jar`; await fs.promises.writeFile(`${this.path}/run.bat`, `@echo off\n${cmd}`); await fs.promises.writeFile(`${this.path}/run.sh`, `#!/bin/bash\n${cmd}`); } private MTP(string: string) { const mjp = string.replace(/^\[|\]$/g, ''); const OriginalName = mjp.split("@")[0]; const x = OriginalName.split(":"); const mappingType = mjp.split('@')[1]; if (x[3]) { return `${x[0].replace(/\./g, '/')}/${x[1]}/${x[2]}/${x[1]}-${x[2]}-${x[3]}.${mappingType}`; } else { return `${x[0].replace(/\./g, '/')}/${x[1]}/${x[2]}/${x[1]}-${x[2]}.${mappingType}`; } } }