-
-
Notifications
You must be signed in to change notification settings - Fork 93
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
22 changed files
with
658 additions
and
251 deletions.
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,148 +1,95 @@ | ||
import { Version } from '@xmcl/core' | ||
import { openEntryReadStream, readAllEntries, readEntry, walkEntriesGenerator } from '@xmcl/unzip' | ||
import { CancelledError, TaskBase } from '@xmcl/task' | ||
import { open, openEntryReadStream, readAllEntries, readEntry, walkEntriesGenerator } from '@xmcl/unzip' | ||
import { createWriteStream } from 'fs' | ||
import { Readable } from 'node:stream' | ||
import { join } from 'path' | ||
import { ZipFile } from 'yauzl' | ||
import { Entry, ZipFile } from 'yauzl' | ||
import { pipeline } from '../util/fs' | ||
import { HMCLModpack, HMCLServerManagedModpack, HMCLVersion } from '/@shared/entities/hmclModpack' | ||
|
||
/** | ||
* https://github.com/huanghongxun/HMCL/wiki/HMCL-%E6%95%B4%E5%90%88%E5%8C%85%E8%A7%84%E8%8C%83 | ||
* The modpack.json | ||
* Read the metadata of the modpack | ||
* @param zip The modpack zip | ||
* @returns The HMCL modpack metadata | ||
*/ | ||
export interface HMCLModpack { | ||
name: string | ||
author: string | ||
/** | ||
* 整合包版本 | ||
*/ | ||
version: string | ||
/** | ||
* 整合包的Minecraft版本,建议填写,在HMCL 2.9以后生效 | ||
*/ | ||
gameVersion: string | ||
/** | ||
* The description. It can be html or markdown. | ||
*/ | ||
description: string | ||
export async function readHMCLModpackMetadata(zip: ZipFile) { | ||
for await (const entry of walkEntriesGenerator(zip)) { | ||
if (entry.fileName === 'server-manifest.json') { | ||
return readEntry(zip, entry).then(b => JSON.parse(b.toString()) as HMCLServerManagedModpack) | ||
} | ||
} | ||
throw new Error() | ||
} | ||
|
||
export interface HMCLServerManagedModpack extends HMCLModpack { | ||
/** | ||
* Your modpack websites. This is displayed to user. | ||
*/ | ||
url?: string[] | string | ||
/** | ||
* If this modpack allow the server update this pack. | ||
* Then this should be modpack root url for each file. | ||
* | ||
* For example, once the modpack is deployed, a file `overrides/config/forge.cfg` is deployed. | ||
* If the fileApi is `https://some.website.com/modpack`, then the file should be avaiable on `https://some.website.com/modpack/overrides/config/forge.cfg` | ||
*/ | ||
fileApi: string | ||
/** | ||
* The update rule | ||
* - `full`, if user modifiess the modpack, then it will be overwrited once the modpack is updated. User added files will be removed! | ||
* - `normal`, user is allowed to modify the modpack in this mode. The modified file won't be overwrited. | ||
* @default full | ||
*/ | ||
update: 'full' | 'normal' | ||
addons: Addon[] | ||
/** | ||
* The library need to install. This is optional | ||
*/ | ||
libraries?: Library[] | ||
/** | ||
* Contains the files need to be downloaded from internet | ||
*/ | ||
files: FileInfo[] | ||
export function resolveHMCLVersion(version: HMCLVersion) { | ||
} | ||
|
||
/** | ||
* Represnet a file need to be downloaded from internet | ||
* This task will install HMCL files into the destination directory. | ||
* | ||
* It will not handle the HMCL version or auto-update function. | ||
*/ | ||
export interface FileInfo { | ||
/** | ||
* The relative path of the file in minecraft | ||
*/ | ||
path: string | ||
/** | ||
* The sha1 of the file | ||
*/ | ||
hash: string | ||
/** | ||
* The file download url | ||
*/ | ||
url?: string | ||
} | ||
export class InstallHMCLModpackTask extends TaskBase<HMCLServerManagedModpack> { | ||
private zip: ZipFile | undefined | ||
private entries: (Entry[]) | undefined = [] | ||
private openedStreams: Readable[] = [] | ||
|
||
export interface Library { | ||
/** | ||
* The library need to be installed. | ||
* | ||
* For example: "cn.skinme.skinme-loader" | ||
*/ | ||
name: string | ||
/** | ||
* The name of the library | ||
*/ | ||
filename: string | ||
hint: 'local' | ||
} | ||
|
||
/** | ||
* An addon will install multiple (thrid party) library to the game. | ||
*/ | ||
export interface Addon { | ||
/** | ||
* "game" for minecraft. "forge" for minecraft forge | ||
*/ | ||
id: string | ||
/** | ||
* The version of the addon. For forge, it has to match BMCLAPI's version | ||
*/ | ||
version: string | ||
} | ||
constructor(path: string, destination: string) { | ||
super() | ||
this._from = path | ||
this._to = destination | ||
} | ||
|
||
export interface PatchedVersion { | ||
protected async ensureZip() { | ||
if (this.zip && this.entries) { | ||
return [this.zip, this.entries] as const | ||
} | ||
this.zip = await open(this._from!) | ||
this.entries = await readAllEntries(this.zip) | ||
|
||
} | ||
return [this.zip, this.entries] as const | ||
} | ||
|
||
export interface PackJson extends Version { | ||
id: string | ||
jar: string | ||
root: boolean | ||
hidden: boolean | ||
patches: PatchedVersion[] | ||
} | ||
protected async track(readStream: Readable) { | ||
readStream.on('data', (b) => { this.update(b.length) }) | ||
this.openedStreams.push(readStream) | ||
} | ||
|
||
function isServerManaged(modpack: HMCLModpack): modpack is HMCLServerManagedModpack { | ||
return 'fileApi' in modpack | ||
} | ||
protected async run(): Promise<HMCLServerManagedModpack> { | ||
const [zip, entries] = await this.ensureZip() | ||
const destination = this._to! | ||
if (entries.find(e => e.fileName === 'server-manifest.json')) { | ||
const promises = [] as Array<Promise<void>> | ||
const fileEntries = entries.filter(e => e.fileName.startsWith('overrides')) | ||
this._total = fileEntries.map(e => e.uncompressedSize).reduce((a, b) => a + b, 0) | ||
for (const e of fileEntries) { | ||
const fileName = join(destination, e.fileName.substring('overrides/'.length, e.fileName.length)) | ||
const readStream = await openEntryReadStream(zip, e) | ||
this.track(readStream) | ||
promises.push(pipeline(readStream, createWriteStream(fileName))) | ||
} | ||
await Promise.all(promises) | ||
const metadata = await readEntry(zip, entries.find(e => e.fileName === 'server-manifest.json')!).then(b => JSON.parse(b.toString()) as HMCLServerManagedModpack) | ||
return metadata | ||
} | ||
throw new Error('Malformed HMCL Modpack!') | ||
} | ||
|
||
export async function readHMCLModpackMetadata(zip: ZipFile) { | ||
for await (const entry of walkEntriesGenerator(zip)) { | ||
if (entry.fileName === 'modpack.json') { | ||
return readEntry(zip, entry).then(b => JSON.parse(b.toString()) as HMCLModpack) | ||
protected async performCancel(): Promise<void> { | ||
for (const stream of this.openedStreams) { | ||
stream.destroy(new CancelledError(undefined)) | ||
} | ||
if (entry.fileName === 'server-manifest.json') { | ||
return readEntry(zip, entry).then(b => JSON.parse(b.toString()) as HMCLServerManagedModpack) | ||
} | ||
|
||
protected async performPause(): Promise<void> { | ||
for (const stream of this.openedStreams) { | ||
stream.pause() | ||
} | ||
} | ||
throw new Error() | ||
} | ||
|
||
export async function installHMCLModpack(zip: ZipFile, destination: string) { | ||
const entries = await readAllEntries(zip) | ||
if (entries.find(e => e.fileName === 'modpack.json')) { | ||
const promises = [] as Array<Promise<void>> | ||
for (const e of entries.filter(e => e.fileName.startsWith('minecraft'))) { | ||
const fileName = join(destination, e.fileName) | ||
if (e.fileName === 'minecraft/pack.json') { | ||
const pack: PackJson = await readEntry(zip, e).then(b => JSON.parse(b.toString())) | ||
} else { | ||
promises.push(pipeline(await openEntryReadStream(zip, e), createWriteStream(fileName))) | ||
} | ||
protected performResume(): void { | ||
for (const stream of this.openedStreams) { | ||
stream.resume() | ||
} | ||
await Promise.all(promises) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.