diff --git a/.github/workflows/build-ontag.yaml b/.github/workflows/build-ontag.yaml index 4826021..176e1ef 100644 --- a/.github/workflows/build-ontag.yaml +++ b/.github/workflows/build-ontag.yaml @@ -47,6 +47,7 @@ jobs: - name: Build app run: | + ./configure.sh node ./build-app.js - name: Tar app diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index e70117f..463a224 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -41,6 +41,7 @@ jobs: - name: Build app run: | + ./configure.sh node ./build-app.js - name: Tar app diff --git a/.gitignore b/.gitignore index 6a23bcf..e2c6564 100644 --- a/.gitignore +++ b/.gitignore @@ -23,4 +23,9 @@ node_modules *.session *.neu -/wine* \ No newline at end of file +/wine* + +/external + +*.log +*.dxvk-cache \ No newline at end of file diff --git a/build-app.js b/build-app.js index 2e81866..c15bed5 100644 --- a/build-app.js +++ b/build-app.js @@ -132,6 +132,7 @@ echo $APST_DIR mkdir -p "$APST_DIR" CONTENTS_DIR="$(dirname "$SCRIPT_DIR")" rsync -rlptu "$CONTENTS_DIR/Resources/." "$APST_DIR" +cd "$APST_DIR" PATH_LAUNCH="$(dirname "$CONTENTS_DIR")" exec "$SCRIPT_DIR/${appname}" --path="$APST_DIR"` ); diff --git a/configure.sh b/configure.sh new file mode 100755 index 0000000..ba4bbc5 --- /dev/null +++ b/configure.sh @@ -0,0 +1,12 @@ +EXTERNAL="./external" + +rm -rf "$EXTERNAL" +mkdir -p "$EXTERNAL" +mkdir -p ./tmp +git clone $(echo "aHR0cHM6Ly9ub3RhYnVnLm9yZy9Lcm9jay9kYXduCg==" | base64 --decode) ./tmp +cp -R ./tmp/350/patch_files/. "$EXTERNAL" +rm -rf ./tmp +pushd "$EXTERNAL" +# for file in * ; do echo "$file" "$(basename $file | base64 )"."${file##*.}" ; done +for file in * ; do mv "$file" "$(basename $file | base64 )"."${file##*.}" ; done +popd diff --git a/dxvk/d3d10core.dll b/dxvk/d3d10core.dll new file mode 100755 index 0000000..27c26c0 Binary files /dev/null and b/dxvk/d3d10core.dll differ diff --git a/dxvk/d3d11.dll b/dxvk/d3d11.dll new file mode 100755 index 0000000..85f8c4c Binary files /dev/null and b/dxvk/d3d11.dll differ diff --git a/dxvk/d3d9.dll b/dxvk/d3d9.dll new file mode 100755 index 0000000..005de86 Binary files /dev/null and b/dxvk/d3d9.dll differ diff --git a/dxvk/dxgi.dll b/dxvk/dxgi.dll new file mode 100755 index 0000000..ae022f9 Binary files /dev/null and b/dxvk/dxgi.dll differ diff --git a/src/app.tsx b/src/app.tsx index 7070fdf..a4012ff 100644 --- a/src/app.tsx +++ b/src/app.tsx @@ -64,16 +64,6 @@ export async function createApp() { github, aria2, }); - // return createCommonUpdateUI(async function *(){ - // yield ['setStateText','测试进度']; - // for(let i=0;i<=100;i++) { - // yield ['setProgress', i]; - // await wait(50); - // } - // yield ['setUndeterminedProgress']; - // await wait(2000); - // yield ['setStateText','完成']; - // }); if (!latest) { if ( await prompt( @@ -85,19 +75,12 @@ export async function createApp() { } } - // return () => ( - //
- // If you are seeing this, it means everything works fine! - //
Current version: {CURRENT_YAAGL_VERSION} - //
- // ); - - const { wineReady, wineUpdate, wineUpdateTag } = await checkWine(); - const prefixPath = await resolve("./wineprefix"); + const { wineReady, wineUpdate, wineUpdateTag } = await checkWine(github); + const prefixPath = await resolve("./wineprefix"); // CHECK: hardcoded path? if (wineReady) { const wine = await createWine({ - installDir: "FIXME", - prefix: "FIXME", + installDir: await resolve("./wine"), // CHECK: hardcoded path? + prefix: prefixPath, }); return await createLauncher({ aria2, wine }); } else { diff --git a/src/constants/server.ts b/src/constants/server.ts index 72f50f1..2b43ed0 100644 --- a/src/constants/server.ts +++ b/src/constants/server.ts @@ -1,3 +1,10 @@ + +import b from "../../external/dW5pdHlwbGF5ZXJfcGF0Y2hfYmIudmNkaWZmCg==.vcdiff?url"; +import c from "../../external/dW5pdHlwbGF5ZXJfcGF0Y2hfY24udmNkaWZmCg==.vcdiff?url"; +import d from "../../external/dW5pdHlwbGF5ZXJfcGF0Y2hfb3MudmNkaWZmCg==.vcdiff?url"; +import e from "../../external/eGx1YV9wYXRjaF9jbi52Y2RpZmYK.vcdiff?url"; +import f from "../../external/eGx1YV9wYXRjaF9vcy52Y2RpZmYK.vcdiff?url"; + export interface Server { id: string; url: string; @@ -87,7 +94,7 @@ export interface ServerContentData { retcode: number; } -export const CN_SERVER: Server = { +export const CN_SERVER = { id: "CN", url: atob( "aHR0cHM6Ly9zZGstc3RhdGljLm1paG95by5jb20vaGs0ZV9jbi9tZGsvbGF1bmNoZXIvYXBpL3Jlc291cmNlP2NoYW5uZWxfaWQ9MSZrZXk9ZVlkODlKbUombGF1bmNoZXJfaWQ9MTg=" @@ -97,9 +104,26 @@ export const CN_SERVER: Server = { bg_url: atob( "aHR0cHM6Ly9zZGstc3RhdGljLm1paG95by5jb20vaGs0ZV9jbi9tZGsvbGF1bmNoZXIvYXBpL2NvbnRlbnQ/ZmlsdGVyX2Fkdj10cnVlJmtleT1lWWQ4OUptSiZsYXVuY2hlcl9pZD0xOCZsYW5ndWFnZT16aC1jbg==" ), + dataDir: atob("WXVhblNoZW5fRGF0YQ=="), + patched: [ + { + file: atob("VW5pdHlQbGF5ZXIuZGxs"), + diffUrl: c, + }, + { + file: atob("WXVhblNoZW5fRGF0YS9QbHVnaW5zL3hsdWEuZGxs"), + diffUrl: e, + }, + ], + removed: [ + "bWh5cGJhc2UuZGxs", + "WXVhblNoZW5fRGF0YS91cGxvYWRfY3Jhc2guZXhl", + "WXVhblNoZW5fRGF0YS9QbHVnaW5zL2NyYXNocmVwb3J0LmV4ZQ==", + "WXVhblNoZW5fRGF0YS9QbHVnaW5zL3Z1bGthbi0xLmRsbA==", + ], }; -export const OS_SERVER: Server = { +export const OS_SERVER = { id: "OS", url: atob( "aHR0cHM6Ly9zZGstb3Mtc3RhdGljLm1paG95by5jb20vaGs0ZV9nbG9iYWwvbWRrL2xhdW5jaGVyL2FwaS9yZXNvdXJjZT9jaGFubmVsX2lkPTEma2V5PWdjU3RnYXJoJmxhdW5jaGVyX2lkPTEw" @@ -107,4 +131,16 @@ export const OS_SERVER: Server = { channel_id: 1, subchannel_id: 0, bg_url: "", + dataDir: atob("R2Vuc2hpbkltcGFjdF9EYXRh"), + patched: [ + { + file: atob("VW5pdHlQbGF5ZXIuZGxs"), + diffUrl: d, + }, + { + file: atob("R2Vuc2hpbkltcGFjdF9EYXRhL1BsdWdpbnMveGx1YS5kbGw="), + diffUrl: f, + }, + ], + removed: [] //FIXME: todo }; diff --git a/src/github.ts b/src/github.ts index 5a7500f..76a7b4d 100644 --- a/src/github.ts +++ b/src/github.ts @@ -21,8 +21,13 @@ export async function createGithubEndpoint() { }) } + function acceleratedPath(path: string) { + return `${fastest}${path}`; + } + return { api, + acceleratedPath }; } diff --git a/src/launcher.tsx b/src/launcher.tsx index 24cc6a6..824b3fa 100644 --- a/src/launcher.tsx +++ b/src/launcher.tsx @@ -1,7 +1,23 @@ import { Aria2 } from "./aria2"; import { Wine } from "./wine"; -import { CN_SERVER, ServerContentData } from "./constants/server"; -import { waitImageReady } from "./utils"; +import { + CN_SERVER, + ServerContentData, + ServerVersionData, +} from "./constants/server"; +import { + getKey, + log, + openDir, + waitImageReady, + alert, + readBinary, + readAllLines, + stats, + fatal, + setKey, + removeFile, +} from "./utils"; import { Box, Button, @@ -16,6 +32,17 @@ import { VStack, } from "@hope-ui/solid"; import { createIcon } from "@hope-ui/solid"; +import { batch, createSignal, onMount, Show } from "solid-js"; +import { join } from "path-browserify"; +import { gt, lt } from "semver"; +import { + patchProgram, + patchRevertProgram, + patternSearch, + putLocal, +} from "./patch"; +import { md5 } from "./utils/unix"; +import { CommonUpdateProgram } from "./common-update-ui"; const IconSetting = createIcon({ viewBox: "0 0 1024 1024", @@ -30,6 +57,23 @@ const IconSetting = createIcon({ }, }); +const CURRENT_SUPPORTED_VERSION = "3.5.0"; + +export async function checkGameState() { + try { + const gameDir = await getKey("game_install_dir"); + return { + gameInstalled: true, + gameInstallDir: gameDir, + gameVersion: await getGameVersion(join(gameDir, CN_SERVER.dataDir)), //FIXME: + } as const; + } catch { + return { + gameInstalled: false, + } as const; + } +} + export async function createLauncher({ aria2, wine, @@ -39,14 +83,111 @@ export async function createLauncher({ }) { const server = CN_SERVER; const b: ServerContentData = await (await fetch(server.bg_url)).json(); + const c: ServerVersionData = await (await fetch(server.url)).json(); + const GAME_LATEST_VERSION = c.data.game.latest.version; await waitImageReady(b.data.adv.background); + const { gameInstalled, gameInstallDir, gameVersion } = await checkGameState(); + return function Laucnher() { // const bh = 40 / window.devicePixelRatio; // const bw = 136 / window.devicePixelRatio; const bh = 40; const bw = 136; + const [statusText, setStatusText] = createSignal(""); + const [progress, setProgress] = createSignal(0); + const [_gameInstalled, setGameInstalled] = createSignal(gameInstalled); + const [_gameInstallDir, setGameInstallDir] = createSignal( + gameInstallDir ?? "" + ); + const [programBusy, setBusy] = createSignal(false); + const [gameCurrentVersion, setGameCurrentVersion] = createSignal( + gameVersion ?? "0.0.0" + ); + + const taskQueue: AsyncGenerator CommonUpdateProgram> = + (async function* () { + const task = yield 0; + setBusy(true); + try { + for await (const text of task()) { + switch (text[0]) { + case "setProgress": + setProgress(text[1]); + break; + case "setUndeterminedProgress": + setProgress(0); + break; + case "setStateText": + setStatusText(text[1]); //FIXME: locales + break; + } + } + } catch (e) { + // fatal + await fatal(e); + return; + } + setBusy(false); + })(); + taskQueue.next(); // ignored anyway + + onMount(() => {}); + + async function onButtonClick() { + if (programBusy()) return; // ignore + if (_gameInstalled()) { + // assert: + taskQueue.next(async function* () { + yield* launchGameProgram({ + gameDir: _gameInstallDir(), + wine, + gameExecutable: atob("WXVhblNoZW4uZXhl"), + }); + }); + } else { + const selection = await openDir("SELECT_INSTALLATION_DIR"); + if (!selection.startsWith("/")) { + await alert("PATH_INVALID", "PLEASE_SELECT_A_DIR"); + return; + } + try { + await stats(join(selection, "pkg_version")); + } catch { + await alert("NOT_SUPPORTED_YET", "DOWNLOAD_FUNCTION_TBD"); + return; + } + const gameVersion = await getGameVersion( + join(selection, server.dataDir) + ); + if (gt(gameVersion, CURRENT_SUPPORTED_VERSION)) { + await alert("UNSUPPORTED_VERSION", "PLEASE_WAIT_FOR_LAUNCHER_UPDATE"); + return; + } else if (lt(gameVersion, GAME_LATEST_VERSION)) { + await alert("NOT_SUPPORTED_YET", "UPGRADE_FUNCTION_TBD"); + return; + } + try { + await stats(join(selection, "pkg_version")); + taskQueue.next(async function* () { + yield* checkIntegrityProgram({ + aria2, + gameDir: selection, + remoteDir: c.data.game.latest.decompressed_path, + }); + // setGameInstalled + batch(() => { + setGameInstalled(true); + setGameInstallDir(selection); + setGameCurrentVersion(gameVersion); + }); + await setKey("game_install_dir", selection); + }); + } catch {} + } + } + return (
Neutralino.os.open(b.data.adv.url)} role="button" class="version-icon" style={{ @@ -64,17 +206,48 @@ export async function createLauncher({ }} >
- + -

Status

- - - + +

+ {statusText()} +

+ + + +
- - - } /> + + + + } + /> +
@@ -83,3 +256,154 @@ export async function createLauncher({ ); }; } + +import a from "../external/bWh5cHJvdDJfcnVubmluZy5yZWcK.reg?url"; +async function* launchGameProgram({ + gameDir, + gameExecutable, + wine, +}: { + gameDir: string; + gameExecutable: string; + wine: Wine; +}): CommonUpdateProgram { + yield ["setUndeterminedProgress"]; + yield ["setStateText", "PATCHING"]; + yield* patchProgram(gameDir, wine.prefix, "cn"); + + await putLocal(a, join(gameDir, "bWh5cHJvdDJfcnVubmluZy5yZWcK.reg")); + try { + await wine.exec("regedit", [ + `"${wine.toWinePath(join(gameDir, "bWh5cHJvdDJfcnVubmluZy5yZWcK.reg"))}"`, + ]); + await removeFile(join(gameDir, "bWh5cHJvdDJfcnVubmluZy5yZWcK.reg")); + await wine.exec("copy", [ + `"${wine.toWinePath(join(gameDir, atob("bWh5cHJvdDMuc3lz")))}"`, + '"%TEMP%\\\\"', + ]); + await wine.exec("copy", [ + `"${wine.toWinePath(join(gameDir, atob("SG9Zb0tQcm90ZWN0LnN5cw==")))}"`, + '"%WINDIR%\\\\system32\\\\"', + ]); + } catch (e) { + yield* patchRevertProgram(gameDir, wine.prefix, "cn"); + throw e; + } + try { + yield ["setStateText", "GAME_RUNNING"]; + const g = wine.toWinePath(join(gameDir, gameExecutable)); + await wine.exec(`"${g}"`, [], { + WINEESYNC: "1", + WINEDEBUG: "-all", + LANG: "zh_CN.UTF-8", + DXVK_HUD: "fps", + MVK_ALLOW_METAL_FENCES: "1", + WINEDLLOVERRIDES: "d3d11,dxgi=n,b", + DXVK_ASYNC: "1", + }); + } catch (e) { + // it seems game crashed? + await log(JSON.stringify(e)); + } + + yield ["setStateText", "REVERT_PATCHING"]; + yield* patchRevertProgram(gameDir, wine.prefix, "cn"); +} + +async function* checkIntegrityProgram({ + gameDir, + remoteDir, + aria2, +}: { + gameDir: string; + remoteDir: string; + aria2: Aria2; +}): CommonUpdateProgram { + const entries: { + remoteName: string; + md5: string; + fileSize: number; + }[] = (await readAllLines(join(gameDir, "pkg_version"))) + .filter((x) => x.trim() != "") + .map((x) => JSON.parse(x)); + const toFix: { + remoteName: string; + md5: string; + }[] = []; + yield ["setStateText", "SCANNING_FILES"]; + let count = 0; + for (const entry of entries) { + const localPath = join(gameDir, entry.remoteName); + try { + const fileStats = await stats(localPath); + if (fileStats.size !== entry.fileSize) { + throw new Error("Size not match"); + } + const md5sum = await md5(localPath); + if (md5sum !== entry.md5) { + await log(`${md5sum} ${entry.md5} not match`); + throw new Error("Md5 not match"); + } + } catch { + toFix.push(entry); + } + count++; + yield ["setProgress", (count / entries.length) * 100]; + } + if (toFix.length == 0) { + return; + } + yield ["setUndeterminedProgress"]; + + yield ["setStateText", "FIXING_FILES"]; + count = 0; + for (const entry of toFix) { + const localPath = join(gameDir, entry.remoteName); + const remotePath = join(remoteDir, entry.remoteName).replace(":/", "://"); //....join: wtf? + await log(remotePath); + await log(localPath); + for await (const progress of aria2.doStreamingDownload({ + uri: remotePath, + absDst: localPath, + })) { + yield [ + "setProgress", + Number((progress.completedLength * BigInt(100)) / progress.totalLength), + ]; + } + count++; + // yield ['setStateText', 'COMPLETE_FILE', count, toFix.length] + } +} + +async function* downloadAndInstallGameProgram() {} + +// async function* +async function checkGameFolder() { + return { + valid: false, + }; +} + +async function getGameVersion(gameDataDir: string) { + const ggmPath = join(gameDataDir, "globalgamemanagers"); + await log(ggmPath); + const view = new Uint8Array(await readBinary(ggmPath)); + await log(`read ${view.byteLength} bytes`); + const index = patternSearch( + view, + [ + 0x69, 0x63, 0x2e, 0x61, 0x70, 0x70, 0x2d, 0x63, 0x61, 0x74, 0x65, 0x67, + 0x6f, 0x72, 0x79, 0x2e, + ] + ); + if (index == -1) { + throw new Error("pattern not found"); //FIXME + } else { + const len = index + 120; + const v = new DataView(view.buffer); + const strlen = v.getUint32(len, true); + const str = String.fromCharCode(...view.slice(len + 4, len + strlen + 4)); + return str.split("_")[0]; + } +} diff --git a/src/patch.ts b/src/patch.ts new file mode 100644 index 0000000..c754279 --- /dev/null +++ b/src/patch.ts @@ -0,0 +1,155 @@ +import { join } from "path-browserify"; +import { CommonUpdateProgram } from "./common-update-ui"; +import { CN_SERVER, OS_SERVER } from "./constants"; +import { + writeBinary, + forceMove, + removeFile, + log, + readBinary, + getKey, + setKey, +} from "./utils"; +import { xdelta3 } from "./utils/unix"; + +import d3d9u from "../dxvk/d3d9.dll?url"; +import d3d10coreu from "../dxvk/d3d10core.dll?url"; +import d3d11u from "../dxvk/d3d11.dll?url"; +import dxgiu from "../dxvk/dxgi.dll?url"; + +export async function putLocal(url: string, dest: string) { + return await writeBinary(dest, await (await fetch(url)).arrayBuffer()); +} + +const patchConfigs = { + cn: CN_SERVER, + os: OS_SERVER, +}; + +const dxvkFiles = [ + { + name: "dxgi", + url: dxgiu, + }, + { + name: "d3d9", + url: d3d9u, + }, + { + name: "d3d10core", + url: d3d10coreu, + }, + { + name: "d3d11", + url: d3d11u, + }, +]; + +export async function* patchProgram( + gameDir: string, + winprefixDir: string, + server: "cn" | "os" +): CommonUpdateProgram { + try { + await getKey("patched"); + return; + } catch {} + for (const file of patchConfigs[server].patched) { + await forceMove( + join(gameDir, file.file), + join(gameDir, file.file + ".bak") + ); + await putLocal(file.diffUrl, join(gameDir, file.file + ".diff")); + await xdelta3( + join(gameDir, file.file + ".bak"), + join(gameDir, file.file + ".diff"), + join(gameDir, file.file) + ); + await log("patched " + file.file); + await removeFile(join(gameDir, file.file + ".diff")); + } + for (const file of patchConfigs[server].removed.map(atob)) { + await forceMove(join(gameDir, file), join(gameDir, file + ".bak")); + } + await forceMove( + join(gameDir, patchConfigs[server].dataDir, "globalgamemanagers"), + join(gameDir, patchConfigs[server].dataDir, "globalgamemanagers.bak") + ); + writeBinary( + join(gameDir, patchConfigs[server].dataDir, "globalgamemanagers"), + await disableUnityFeature( + join(gameDir, patchConfigs[server].dataDir, "globalgamemanagers.bak") + ) + ); + const system32Dir = join(winprefixDir, "drive_c", "windows", "system32"); + for (const f of dxvkFiles) { + await forceMove( + join(system32Dir, f.name + ".dll"), + join(system32Dir, f.name + ".dll.bak") + ); + await putLocal(f.url, join(system32Dir, f.name + ".dll")); + } + setKey("patched", "1"); +} + +export async function* patchRevertProgram( + gameDir: string, + winprefixDir: string, + server: "cn" | "os" +): CommonUpdateProgram { + try { + await getKey("patched"); + } catch { + return; + } + for (const file of patchConfigs[server].patched) { + await forceMove( + join(gameDir, file.file + ".bak"), + join(gameDir, file.file) + ); + } + for (const file of patchConfigs[server].removed.map(atob)) { + await forceMove(join(gameDir, file + ".bak"), join(gameDir, file)); + } + await forceMove( + join(gameDir, patchConfigs[server].dataDir, "globalgamemanagers.bak"), + join(gameDir, patchConfigs[server].dataDir, "globalgamemanagers") + ); + const system32Dir = join(winprefixDir, "drive_c", "windows", "system32"); + for (const f of dxvkFiles) { + await forceMove( + join(system32Dir, f.name + ".dll.bak"), + join(system32Dir, f.name + ".dll") + ); + } + setKey("patched", null); +} + +async function disableUnityFeature(ggmPath: string) { + const view = new Uint8Array(await readBinary(ggmPath)); + const index = patternSearch( + view, + [ + 0x69, 0x63, 0x2e, 0x61, 0x70, 0x70, 0x2d, 0x63, 0x61, 0x74, 0x65, 0x67, + 0x6f, 0x72, 0x79, 0x2e, + ] + ); + if (index == -1) { + throw new Error("pattern not found"); //FIXME + } else { + const len = index + 8; + const v = new DataView(view.buffer); + v.setInt32(len, 0, true); + return view.buffer; + } +} + +export function patternSearch(view: Uint8Array, pattern: number[]) { + retry: for (let i = 0; i < view.byteLength - pattern.length; i++) { + for (let j = 0; j < pattern.length; j++) { + if (view[i + j] != pattern[j]) continue retry; + } + return i + pattern.length; + } + return -1; +} diff --git a/src/utils/neu.ts b/src/utils/neu.ts index 5298e07..40ef893 100644 --- a/src/utils/neu.ts +++ b/src/utils/neu.ts @@ -22,16 +22,18 @@ export async function exec( .map((key) => { return `${key}=${env[key]} `; }) - .join() + .join("") : "" }"${await resolve(command)}" ${args .map((x) => { - if (x.startsWith('"')||x.startsWith("'")) return x; + if (x.startsWith('"') || x.startsWith("'")) return x; if (x.indexOf(" ") > -1) return `"${x}"`; return x; }) .join(" ")}`; - const ret = sudo ? await runInSudo(cmd) : await Neutralino.os.execCommand(cmd, {}); + const ret = sudo + ? await runInSudo(cmd) + : await Neutralino.os.execCommand(cmd, {}); if (ret.exitCode != 0) { throw new Error( `Command return non-zero code\n${cmd}\nStdOut:\n${ret.stdOut}\nStdErr:\n${ret.stdErr}` @@ -41,9 +43,7 @@ export async function exec( } export async function runInSudo(command: string) { - command = command - .replaceAll('"', "\\\\\\\"") - .replaceAll("'", "\\'"); + command = command.replaceAll('"', '\\\\\\"').replaceAll("'", "\\'"); await log(command); return await Neutralino.os.execCommand( `osascript -e $'do shell script "${command}" with administrator privileges'`, @@ -91,7 +91,7 @@ export function restart() { export async function fatal(error: any) { await Neutralino.os.showMessageBox( "Fatal error", - `${String(error)}`, + `${error instanceof Error ? String(error) : JSON.stringify(error)}`, "OK" ); await shutdown(); @@ -119,6 +119,39 @@ export async function prompt(title: string, message: string) { return out == "YES"; } +export async function alert(title: string, message: string) { + return await Neutralino.os.showMessageBox(title, message, "OK"); +} + +export async function openDir(title: string) { + const out = await Neutralino.os.showFolderDialog(title, {}); + return out; +} + +export async function readBinary(path: string) { + return await Neutralino.filesystem.readBinaryFile(path); +} + +export async function readAllLines(path: string) { + const content = await Neutralino.filesystem.readFile(path); + if (content.indexOf("\r\n") >= 0) { + return content.split("\r\n"); + } + return content.split("\n"); +} + +export async function writeBinary(path: string, data: ArrayBuffer) { + return await Neutralino.filesystem.writeBinaryFile(path, data); +} + +export async function removeFile(path: string) { + return await Neutralino.filesystem.removeFile(path); +} + +export async function stats(path: string) { + return await Neutralino.filesystem.getStats(path); +} + const hooks: Array<(forced: boolean) => Promise> = []; export function addTerminationHook(fn: (forced: boolean) => Promise) { diff --git a/src/utils/unix.ts b/src/utils/unix.ts index b149ccd..10c7d10 100644 --- a/src/utils/unix.ts +++ b/src/utils/unix.ts @@ -6,7 +6,7 @@ export async function xattrRemove(attr: string, path: string) { export async function md5(path: string): Promise { const p = await exec("md5", ["-q", await resolve(path)]); - return p.stdOut; + return p.stdOut.split('\n')[0]; } export async function xdelta3( diff --git a/src/wine.tsx b/src/wine.tsx index 2459352..ff32003 100644 --- a/src/wine.tsx +++ b/src/wine.tsx @@ -1,7 +1,9 @@ +import { join } from "path-browserify"; import { Aria2 } from "./aria2"; import { CommonUpdateProgram, createCommonUpdateUI } from "./common-update-ui"; +import { Github } from "./github"; import { - exec, + exec as unixExec, fatal, getKey, log, @@ -18,18 +20,42 @@ export async function createWine(options: { installDir: string; prefix: string; }) { - async function cmd(command: string, args: string[]) {} + async function cmd(command: string, args: string[]) { + return await exec("cmd", [command, ...args]); + } - async function launch(program: string) {} + async function exec( + program: string, + args: string[], + env?: { [key: string]: string } + ) { + return await unixExec( + join(options.installDir, "bin/wine64"), + program == "copy" ? ["cmd", "/c", program, ...args] : [program, ...args], + { + WINEPREFIX: `"${options.prefix}"`, + ...(env ?? {}), + } + ); + } - return {}; + function toWinePath(absPath: string) { + return "Z:" + absPath.replaceAll("/", "\\"); + } + + return { + exec, + cmd, + toWinePath, + prefix: options.prefix, + }; } export type Wine = ReturnType extends Promise ? T : never; -export async function checkWine() { +export async function checkWine(github: Github) { // TODO try { const wineState = await getKey("wine_state"); @@ -45,8 +71,9 @@ export async function checkWine() { // FIXME: return { wineReady: false, - wineUpdate: - "https://github.com/3Shain/winecx/releases/download/gi-wine-1.0/wine.tar.gz", + wineUpdate: github.acceleratedPath( + "https://github.com/3Shain/winecx/releases/download/gi-wine-1.0/wine.tar.gz" + ), wineUpdateTag: "gi-wine-1.0", } as const; } @@ -80,7 +107,7 @@ export async function createWineInstallProgram({ const wineBinaryDir = await resolve("./wine"); await rmrf_dangerously(wineBinaryDir); - await exec("mkdir", ["-p", wineBinaryDir]); + await unixExec("mkdir", ["-p", wineBinaryDir]); const p = await tar_extract(await resolve("./wine.tar.gz"), wineBinaryDir); await log(p.stdOut); yield ["setStateText", "CONFIGURING_ENVIROMENT"]; @@ -88,17 +115,13 @@ export async function createWineInstallProgram({ await xattrRemove("com.apple.quarantine", wineBinaryDir); const wine64Bin = await resolve("./wine/bin/wine64"); - const d = await exec( - wine64Bin, - ["wineboot", "-u"], - { WINEPREFIX: `"${wineAbsPrefix}"` } - ); + const d = await unixExec(wine64Bin, ["wineboot", "-u"], { + WINEPREFIX: `"${wineAbsPrefix}"`, + }); await log(d.stdOut); - const g = await exec( - wine64Bin, - ["winecfg", "-v", "win10"], - { WINEPREFIX: `"${wineAbsPrefix}"` } - ); + const g = await unixExec(wine64Bin, ["winecfg", "-v", "win10"], { + WINEPREFIX: `"${wineAbsPrefix}"`, + }); await log(g.stdOut); await setKey("wine_state", "ready"); await setKey("wine_tag", wineTag);