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);