diff --git a/.gitignore b/.gitignore index 5d2754a..6bb0978 100644 --- a/.gitignore +++ b/.gitignore @@ -26,4 +26,6 @@ trace downloads .flatpak-builder gameflow-deck.code-workspace -.env.local \ No newline at end of file +.env.local +src/tests/mock-roms/db.sqlite +src/tests/mock-config \ No newline at end of file diff --git a/bun.lock b/bun.lock index f3813b7..9741115 100644 --- a/bun.lock +++ b/bun.lock @@ -5,19 +5,22 @@ "": { "name": "electrobun-hello-world", "dependencies": { - "7zip-min": "^3.0.1", + "7zip-bin": "^5.2.0", "@auth/core": "^0.34.3", "@elysiajs/cors": "^1.4.1", "@elysiajs/eden": "^1.4.6", "@jimp/wasm-webp": "^1.6.0", + "@kmamal/sdl": "^0.11.13", "cheerio": "^1.2.0", "conf": "^15.0.2", "drizzle-orm": "^0.45.1", "elysia": "^1.4.22", "fs-extra": "^11.3.3", "get-folder-size": "^5.0.0", + "ini": "^6.0.0", "jimp": "^1.6.0", "mustache": "^4.2.0", + "node-7z": "^3.0.0", "node-disk-info": "^1.3.0", "node-downloader-helper": "^2.1.10", "node-stream-zip": "^1.15.0", @@ -49,7 +52,9 @@ "@tanstack/zod-adapter": "^1.162.4", "@types/bun": "latest", "@types/fs-extra": "^11.0.4", + "@types/ini": "^4.1.1", "@types/mustache": "^4.2.6", + "@types/node-7z": "^2.1.11", "@types/react": "^19.2.9", "@types/react-dom": "^19.2.3", "@types/unzip-stream": "^0.3.4", @@ -86,9 +91,7 @@ }, }, "packages": { - "7zip-bin": ["7zip-bin@5.1.1", "", {}, "sha512-sAP4LldeWNz0lNzmTird3uWfFDWWTeg6V/MsmyyLR9X1idwKBWIgt/ZvinqQldJm3LecKEs1emkbquO6PCiLVQ=="], - - "7zip-min": ["7zip-min@3.0.1", "", { "dependencies": { "7zip-bin": "5.1.1" } }, "sha512-WB4VCA/KSKzxhj+BAp8fI3ZYMMAftclkXlUTckuiDacsqyubQxxG3lGcpBcgzWWuJqnfQncEq1xrJpPLSxqsxw=="], + "7zip-bin": ["7zip-bin@5.2.0", "", {}, "sha512-ukTPVhqG4jNzMro2qA9HSCSSVJN3aN7tlb+hfqYCt3ER0yWroeA2VR38MNrOHLQ/cVj+DaIMad0kFCtWWowh/A=="], "@ap0nia/eden": ["@ap0nia/eden@1.0.0-next.22", "", { "peerDependencies": { "elysia": "^1.3.1" } }, "sha512-9iH09koK29Yuem80fz8nCt9iHVcJqxUo2QHAr4psI02PhvL70n6aWVo/hlHyYXwOSsSgRQlLl1vPmiulFOUFoA=="], @@ -324,6 +327,8 @@ "@isaacs/cliui": ["@isaacs/cliui@8.0.2", "", { "dependencies": { "string-width": "^5.1.2", "string-width-cjs": "npm:string-width@^4.2.0", "strip-ansi": "^7.0.1", "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", "wrap-ansi": "^8.1.0", "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" } }, "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA=="], + "@isaacs/fs-minipass": ["@isaacs/fs-minipass@4.0.1", "", { "dependencies": { "minipass": "^7.0.4" } }, "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w=="], + "@jimp/core": ["@jimp/core@1.6.0", "", { "dependencies": { "@jimp/file-ops": "1.6.0", "@jimp/types": "1.6.0", "@jimp/utils": "1.6.0", "await-to-js": "^3.0.0", "exif-parser": "^0.1.12", "file-type": "^16.0.0", "mime": "3" } }, "sha512-EQQlKU3s9QfdJqiSrZWNTxBs3rKXgO2W+GxNXDtwchF3a4IqxDheFX1ti+Env9hdJXDiYLp2jTRjlxhPthsk8w=="], "@jimp/diff": ["@jimp/diff@1.6.0", "", { "dependencies": { "@jimp/plugin-resize": "1.6.0", "@jimp/types": "1.6.0", "@jimp/utils": "1.6.0", "pixelmatch": "^5.3.0" } }, "sha512-+yUAQ5gvRC5D1WHYxjBHZI7JBRusGGSLf8AmPRPCenTzh4PA+wZ1xv2+cYqQwTfQHU5tXYOhA0xDytfHUf1Zyw=="], @@ -398,6 +403,8 @@ "@jsquash/webp": ["@jsquash/webp@1.5.0", "", { "dependencies": { "wasm-feature-detect": "^1.2.11" } }, "sha512-KggLoj2MnRSfIqTeKe1EmbljTX2vuV7mh79k89PCL1pyqiDULcPM1L47twxXt0hkb68F70bXiL31MxsuoZtKFw=="], + "@kmamal/sdl": ["@kmamal/sdl@0.11.13", "", { "dependencies": { "tar": "^7.4.3" } }, "sha512-9WmxYNtCggi7Ovq1cU7m/s5WXD/+eKQxDMnL3bU8B5vr5GlaLg4xLykDCpcbWKkJJ2i6llTrdL7LiqikwDFz4w=="], + "@node-minify/clean-css": ["@node-minify/clean-css@9.0.1", "", { "dependencies": { "@node-minify/utils": "9.0.1", "clean-css": "5.3.3" } }, "sha512-GHTMmjGloRvNzqdG7foI0iZeS2QmuYCQvdASJP9sCKjkpH45bygODpXPYKnlzUEpQgYvPK9Q3GxqYnVY9SdoqA=="], "@node-minify/core": ["@node-minify/core@9.0.2", "", { "dependencies": { "@node-minify/utils": "9.0.1", "glob": "10.3.3", "mkdirp": "3.0.1" } }, "sha512-FNhv29Wom6wKrrFKaeAfmZqz7TX5A1E6P+bpd0VIc+DYWMLUIhAViS8riaZg3A1oD0s06s+5BG2Fg7RqMKiKHw=="], @@ -600,6 +607,8 @@ "@types/fs-extra": ["@types/fs-extra@11.0.4", "", { "dependencies": { "@types/jsonfile": "*", "@types/node": "*" } }, "sha512-yTbItCNreRooED33qjunPthRcSjERP1r4MqCZc7wv0u2sUkzTFp45tgUfS5+r7FrZPdmCCNflLhVSP/o+SemsQ=="], + "@types/ini": ["@types/ini@4.1.1", "", {}, "sha512-MIyNUZipBTbyUNnhvuXJTY7B6qNI78meck9Jbv3wk0OgNwRyOOVEKDutAkOs1snB/tx0FafyR6/SN4Ps0hZPeg=="], + "@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="], "@types/jsonfile": ["@types/jsonfile@6.1.4", "", { "dependencies": { "@types/node": "*" } }, "sha512-D5qGUYwjvnNNextdU59/+fI+spnwtTFmyQP0h+PfIOSkNfpU6AOICUOkm4i0OnSk+NyjdPJrxCDro0sJsWlRpQ=="], @@ -610,6 +619,8 @@ "@types/node": ["@types/node@24.1.0", "", { "dependencies": { "undici-types": "~7.8.0" } }, "sha512-ut5FthK5moxFKH2T1CUOC6ctR67rQRvvHdFLCD2Ql6KXmMuCrjsSsRI9UsLCm9M18BMwClv4pn327UvB7eeO1w=="], + "@types/node-7z": ["@types/node-7z@2.1.11", "", { "dependencies": { "@types/node": "*" } }, "sha512-7gwLx44tqZqjyrvjkX41CWW4h7+aXrazFg/JR6N5g+R5BW1eqsNuw8SNLWrh7KcnfKhAYFiWyNb10ti5v5eCmQ=="], + "@types/normalize-package-data": ["@types/normalize-package-data@2.4.4", "", {}, "sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA=="], "@types/react": ["@types/react@19.2.9", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-Lpo8kgb/igvMIPeNV2rsYKTgaORYdO1XGVZ4Qz3akwOj0ySGYMPlQWa8BaLn0G63D1aSaAQ5ldR06wCpChQCjA=="], @@ -728,6 +739,8 @@ "chokidar": ["chokidar@3.6.0", "", { "dependencies": { "anymatch": "~3.1.2", "braces": "~3.0.2", "glob-parent": "~5.1.2", "is-binary-path": "~2.1.0", "is-glob": "~4.0.1", "normalize-path": "~3.0.0", "readdirp": "~3.6.0" }, "optionalDependencies": { "fsevents": "~2.3.2" } }, "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw=="], + "chownr": ["chownr@3.0.0", "", {}, "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g=="], + "citty": ["citty@0.1.6", "", { "dependencies": { "consola": "^3.2.3" } }, "sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ=="], "classnames": ["classnames@2.5.1", "", {}, "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow=="], @@ -1050,7 +1063,7 @@ "inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="], - "ini": ["ini@1.3.8", "", {}, "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew=="], + "ini": ["ini@6.0.0", "", {}, "sha512-IBTdIkzZNOpqm7q3dRqJvMaldXjDHWkEDfrwGEQTs5eaQMWV+djAhR+wahyNNMAa+qpbDUhBMVt4ZKNwpPm7xQ=="], "is-arrayish": ["is-arrayish@0.2.1", "", {}, "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg=="], @@ -1206,6 +1219,8 @@ "minipass": ["minipass@7.1.3", "", {}, "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A=="], + "minizlib": ["minizlib@3.1.0", "", { "dependencies": { "minipass": "^7.1.2" } }, "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw=="], + "mkdirp": ["mkdirp@0.5.6", "", { "dependencies": { "minimist": "^1.2.6" }, "bin": { "mkdirp": "bin/cmd.js" } }, "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw=="], "modify-values": ["modify-values@1.0.1", "", {}, "sha512-xV2bxeN6F7oYjZWTe/YPAy6MN2M+sL4u/Rlm2AHCIVGfo2p1yGmBHQ6vHehl4bRTZBdHu3TSkWdYgkwpYzAGSw=="], @@ -1544,6 +1559,8 @@ "tapable": ["tapable@2.3.0", "", {}, "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg=="], + "tar": ["tar@7.5.13", "", { "dependencies": { "@isaacs/fs-minipass": "^4.0.0", "chownr": "^3.0.0", "minipass": "^7.1.2", "minizlib": "^3.1.0", "yallist": "^5.0.0" } }, "sha512-tOG/7GyXpFevhXVh8jOPJrmtRpOTsYqUIkVdVooZYJS/z8WhfQUX8RJILmeuJNinGAMSu1veBr4asSHFt5/hng=="], + "terser": ["terser@5.46.0", "", { "dependencies": { "@jridgewell/source-map": "^0.3.3", "acorn": "^8.15.0", "commander": "^2.20.0", "source-map-support": "~0.5.20" }, "bin": { "terser": "bin/terser" } }, "sha512-jTwoImyr/QbOWFFso3YoU3ik0jBBDJ6JTOQiy/J2YxVJdZCc+5u7skhNwiOR3FQIygFqVUPHl7qbbxtjW2K3Qg=="], "text-extensions": ["text-extensions@1.9.0", "", {}, "sha512-wiBrwC1EhBelW12Zy26JeOUkQ5mRu+5o8rpsJk5+2t+Y5vE7e842qtZDQ2g1NpX/29HdyFeJ4nSIhI47ENSxlQ=="], @@ -1674,7 +1691,7 @@ "y18n": ["y18n@5.0.8", "", {}, "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA=="], - "yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="], + "yallist": ["yallist@5.0.0", "", {}, "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw=="], "yargs": ["yargs@17.7.2", "", { "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.3", "y18n": "^5.0.5", "yargs-parser": "^21.1.1" } }, "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w=="], @@ -1788,6 +1805,8 @@ "git-semver-tags/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], + "gitconfiglocal/ini": ["ini@1.3.8", "", {}, "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew=="], + "glob/minimatch": ["minimatch@9.0.9", "", { "dependencies": { "brace-expansion": "^2.0.2" } }, "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg=="], "handlebars/source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="], @@ -1802,6 +1821,8 @@ "load-json-file/pify": ["pify@3.0.0", "", {}, "sha512-C3FsVNH1udSEX48gGX1xfvwTWfsYWj5U+8/uK15BGzIGrKoUpghX8hWZwa/OFnakBiiVNmBvemTJR5mcy7iPcg=="], + "lru-cache/yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="], + "meow/read-pkg-up": ["read-pkg-up@7.0.1", "", { "dependencies": { "find-up": "^4.1.0", "read-pkg": "^5.2.0", "type-fest": "^0.8.1" } }, "sha512-zK0TB7Xd6JpCLmlLmufqykGE+/TlOePD6qKClNW7hHDKFh/J7/7gCWGR7joEQEW1bKq3a3yUZSObOoWLFQ4ohg=="], "meow/type-fest": ["type-fest@0.18.1", "", {}, "sha512-OIAYXk8+ISY+qTOwkHtKqzAuxchoMiD9Udx+FSGQDuiRR+PJKJHc2NJAXlbhkGwTt/4/nKZxELY1w3ReWOL8mw=="], diff --git a/package.json b/package.json index bac8d98..1b39dbb 100644 --- a/package.json +++ b/package.json @@ -13,8 +13,10 @@ "packageManager": "bun@1.3.9", "type": "module", "scripts": { - "dev": " NODE_ENV=development bun run build:vite && conc 'bun run ./scripts/dev.ts'", + "dev": "NODE_ENV=development bun run build:vite && conc 'bun run ./scripts/dev.ts'", "dev:hmr": "PUBLIC_ACCESS=true conc -k 'bun run hmr' 'bun run ./scripts/dev.ts'", + "dev:bun:hmr": "PUBLIC_ACCESS=true NODE_ENV=development conc 'bun run hmr' 'bun run --watch ./src/bun/index.ts", + "dev:bun": "NODE_ENV=development bun run build:vite && conc 'bun run ./src/bun/index.ts", "build:vite": "bun run --bun vite build", "build:prod:vite": "NODE_ENV=production bun run build:vite", "build:dev:vite": "NODE_ENV=development bun run build:vite", @@ -40,19 +42,22 @@ "package:Windows": "bun run build:prod" }, "dependencies": { - "7zip-min": "^3.0.1", + "7zip-bin": "^5.2.0", "@auth/core": "^0.34.3", "@elysiajs/cors": "^1.4.1", "@elysiajs/eden": "^1.4.6", "@jimp/wasm-webp": "^1.6.0", + "@kmamal/sdl": "^0.11.13", "cheerio": "^1.2.0", "conf": "^15.0.2", "drizzle-orm": "^0.45.1", "elysia": "^1.4.22", "fs-extra": "^11.3.3", "get-folder-size": "^5.0.0", + "ini": "^6.0.0", "jimp": "^1.6.0", "mustache": "^4.2.0", + "node-7z": "^3.0.0", "node-disk-info": "^1.3.0", "node-downloader-helper": "^2.1.10", "node-stream-zip": "^1.15.0", @@ -84,7 +89,9 @@ "@tanstack/zod-adapter": "^1.162.4", "@types/bun": "latest", "@types/fs-extra": "^11.0.4", + "@types/ini": "^4.1.1", "@types/mustache": "^4.2.6", + "@types/node-7z": "^2.1.11", "@types/react": "^19.2.9", "@types/react-dom": "^19.2.3", "@types/unzip-stream": "^0.3.4", diff --git a/scripts/dev.ts b/scripts/dev.ts index 5357463..735efa1 100644 --- a/scripts/dev.ts +++ b/scripts/dev.ts @@ -2,6 +2,8 @@ import EventEmitter from "events"; import browser from '../src/bun/browser'; import { tmpdir } from "os"; import path from "path"; +import { createInterface } from "readline"; +import { Readable } from "stream"; const events = new EventEmitter(); const abortController = new AbortController(); @@ -12,23 +14,16 @@ let retries = 0; function spawnServer () { - return Bun.spawn(["bun", '--watch', '--install=fallback', "run", "--inspect=127.0.0.1:9228/fixed-session", "./src/bun/index.ts"], { + const s = Bun.spawn(["bun", '--watch', '--install=fallback', "run", "--inspect=127.0.0.1:9228/fixed-session", "./src/bun/index.ts"], { env: { ...process.env, HEADLESS: "true", }, - stdout: "inherit", + stdout: "pipe", stderr: "inherit", stdin: "pipe", signal: abortController.signal, killSignal: 'SIGUSR1', - ipc (message, subprocess, handle) - { - if (message.type === 'exitapp') - { - events.emit('exitapp'); - } - }, onExit (subprocess, exitCode, signalCode) { if (exitCode === 1 && retries <= 3) @@ -42,6 +37,18 @@ function spawnServer () } }); + const rl = createInterface({ input: Readable.fromWeb(s.stdout as any) }); + rl.on('line', e => + { + if (e === 'focus') + { + events.emit('focus'); + } else + { + console.log(e); + } + }); + return s; } function spawnBrowser () diff --git a/scripts/package-bun.ts b/scripts/package-bun.ts index e83eb8b..c70df95 100644 --- a/scripts/package-bun.ts +++ b/scripts/package-bun.ts @@ -26,13 +26,30 @@ if (process.env.TARGET) compileOption.target = process.env.TARGET as any; } -let webviewLib = "libwebview.dll"; -if (process.platform === 'linux' && system.arch === 'x64') - webviewLib = "libwebview-x64.so"; -if (process.platform === 'linux' && system.arch === 'arm64') - webviewLib = "libwebview-arm64.so"; -if (process.platform === 'darwin') - webviewLib = "libwebview-arm64.dylib"; + +let zip: string | undefined; +let zipNodePath: string | undefined; +let webviewLib: string | undefined; +switch (process.platform) +{ + case "win32": + zip = "7za.exe"; + zipNodePath = "win"; + webviewLib = `libwebview.dll`; + break; + case "linux": + zip = "7za"; + zipNodePath = 'linux'; + webviewLib = `libwebview-${system.arch}.so`; + break; + case "darwin": + zip = "7za"; + zipNodePath = 'mac'; + webviewLib = `libwebview-${system.arch}.dylib`; + break; +} + +if (!webviewLib) throw new Error("Could not find webviewlib"); let webviewLibPath = '.'; if (process.env.APPIMAGE === "true") @@ -47,6 +64,7 @@ await Bun.build({ define: { "process.env.IS_BINARY": "true", "process.env.WEBVIEW_PATH": `${webviewLibPath}/${webviewLib}`, + "process.env.ZIP7_PATH": `"${zip}"` }, minify: process.env.NODE_ENV !== 'development', sourcemap: process.env.NODE_ENV === 'development' ? 'inline' : "linked", @@ -77,6 +95,8 @@ await Bun.build({ await fs.cp('./drizzle', `${buildSubDir}/drizzle`, { recursive: true }); await fs.cp(`./vendors/es-de/emulators.${system.platform}.${system.arch}.sqlite`, `${buildSubDir}/vendors/es-de/emulators.${system.platform}.${system.arch}.sqlite`, { recursive: true }); await fs.cp(path.join(`node_modules/webview-bun/build/`, webviewLib), path.join(buildSubDir, webviewLib)); + await fs.cp(`node_modules/@kmamal/sdl/dist`, buildSubDir, { recursive: true, errorOnExist: false }); + await fs.cp(`node_modules/7zip-bin/${zipNodePath}/${process.arch}`, buildSubDir, { recursive: true, errorOnExist: false }); }); }, }] diff --git a/src/bun/api/app.ts b/src/bun/api/app.ts index 37b09d6..597a475 100644 --- a/src/bun/api/app.ts +++ b/src/bun/api/app.ts @@ -22,10 +22,12 @@ import UpdateStoreJob from "./jobs/update-store"; import { getStoreFolder } from "./store/services/gamesService"; import { PluginManager } from "./plugins/plugin-manager"; import registerPlugins from "./plugins/register-plugins"; +import controls from '../controls'; export const config = new Conf({ projectName: projectPackage.name, projectSuffix: 'bun', + cwd: process.env.CONFIG_CWD, schema: Object.fromEntries(Object.entries(SettingsSchema.shape).map(([key, schema]) => [key, schema.toJSONSchema() as any])) as any, defaults: SettingsSchema.parse({ downloadPath: path.join(os.homedir(), "gameflow"), @@ -35,6 +37,7 @@ export const config = new Conf({ export const customEmulators = new Conf>({ projectName: projectPackage.name, projectSuffix: 'bun', + cwd: process.env.CONFIG_CWD, configName: 'custom-emulators', rootSchema: { "type": "object", @@ -67,6 +70,7 @@ registerPlugins(plugins); export const events = new EventEmitter(); config.onDidChange('downloadPath', () => reloadDatabase()); taskQueue.enqueue(UpdateStoreJob.id, new UpdateStoreJob()); +await controls(); export async function cleanup () { diff --git a/src/bun/api/games/services/launchGameService.ts b/src/bun/api/games/services/launchGameService.ts index 8269285..c1b027f 100644 --- a/src/bun/api/games/services/launchGameService.ts +++ b/src/bun/api/games/services/launchGameService.ts @@ -73,10 +73,6 @@ export async function getEmulatorsForSystem (systemSlug: string) export async function getValidLaunchCommands (data: { systemSlug: string; gamePath: string; - customEmulatorConfig: { - get: (id: string) => string | undefined, - has: (id: string) => boolean, - }; }): Promise { diff --git a/src/bun/api/jobs/emulator-download-job.ts b/src/bun/api/jobs/emulator-download-job.ts index 270d1ee..54af36d 100644 --- a/src/bun/api/jobs/emulator-download-job.ts +++ b/src/bun/api/jobs/emulator-download-job.ts @@ -6,7 +6,7 @@ import { Glob } from "bun"; import { config } from "../app"; import path from 'node:path'; import { getOrCachedGithubRelease } from "../cache"; -import _7z from '7zip-min'; +import Seven from 'node-7z'; import fs from "node:fs/promises"; import { Downloader } from "@/bun/utils/downloader"; import { move } from "fs-extra"; @@ -85,7 +85,13 @@ export class EmulatorDownloadJob implements IJob + { + const seven = Seven.extractFull(destinationPath, emulatorsFolder, { $bin: process.env.ZIP7_PATH, $progress: true }); + seven.on('progress', p => context.setProgress(p.percent, "extract")); + seven.on('error', e => reject(e)); + seven.on('end', () => resolve(true)); + }); await fs.rm(destinationPath, { recursive: true }); // check if 1 root folder we need to get rid of diff --git a/src/bun/api/jobs/install-job.ts b/src/bun/api/jobs/install-job.ts index 430aa40..2383b08 100644 --- a/src/bun/api/jobs/install-job.ts +++ b/src/bun/api/jobs/install-job.ts @@ -1,5 +1,4 @@ import { IJob, JobContext } from "../task-queue"; -import { mkdir } from 'node:fs/promises'; import { and, eq, or } from 'drizzle-orm'; import fs from 'node:fs/promises'; import * as schema from "@schema/app"; @@ -11,11 +10,10 @@ import * as igdb from 'ts-igdb-client'; import secrets from "../secrets"; import { simulateProgress } from "@/bun/utils"; import { Downloader } from "@/bun/utils/downloader"; -import _7z from '7zip-min'; +import Seven from 'node-7z'; import z from "zod"; import { checkFiles } from "../games/services/utils"; import { ensureDir } from "fs-extra"; -import { getAuthToken } from "@/clients/romm/core/auth.gen"; interface JobConfig { @@ -105,9 +103,19 @@ export class InstallJob implements IJob const downloadedFiles = await downloader.start(); if (info.extract_path && downloadedFiles) { + let progress = 0; + const progressDelta = 1 / downloadedFiles.length; for (const path of downloadedFiles) { - await _7z.unpack(path, info.extract_path); + const extractPath = info.extract_path; + await new Promise((resolve, reject) => + { + const seven = Seven.extractFull(path, extractPath, { $bin: process.env.ZIP7_PATH, $progress: true }); + seven.on('progress', p => cx.setProgress(progress + p.percent * progressDelta, "extract")); + seven.on('error', e => reject(e)); + seven.on('end', () => resolve(true)); + }); + progress += progressDelta * 100; } } } diff --git a/src/bun/api/jobs/launch-game-job.ts b/src/bun/api/jobs/launch-game-job.ts index 75b6ff0..4265ae3 100644 --- a/src/bun/api/jobs/launch-game-job.ts +++ b/src/bun/api/jobs/launch-game-job.ts @@ -4,7 +4,8 @@ import { ActiveGameSchema, ActiveGameType } from "@/bun/types/typesc.schema"; import { db, events, plugins } from "../app"; import * as appSchema from "@schema/app"; import { eq, sql } from "drizzle-orm"; -import { spawn } from 'node:child_process'; +import { ChildProcessWithoutNullStreams, spawn } from 'node:child_process'; +import { killBrowser } from "@/bun/utils/browser-spawner"; export class LaunchGameJob implements IJob, "playing"> { @@ -39,26 +40,42 @@ export class LaunchGameJob implements IJob { - const game = spawn(command, commandArgs, { - shell: true, - cwd: this.validCommand.startDir, - signal: context.abortSignal - }); + let game: Bun.Subprocess; + if (!commandArgs) + { + game = Bun.spawn(this.validCommand.command.split(' '), { + cwd: this.validCommand.startDir, + windowsVerbatimArguments: true, + signal: context.abortSignal + }); - game.stdout.on('data', data => console.log(data)); - game.on('close', (code) => + game.exited.then(resolve).catch(e => + { + console.error(e); + reject(e); + }); + } + else if (this.validCommand.metadata.emulatorBin) { - resolve(code); - }); - game.on('error', e => + game = Bun.spawn([this.validCommand.metadata.emulatorBin, ...commandArgs], { + cwd: this.validCommand.startDir, + windowsVerbatimArguments: true, + signal: context.abortSignal + }); + + game.exited.then(resolve).catch(e => + { + console.error(e); + reject(e); + }); + } else { - console.error(e); - reject(e); - }); + reject(new Error("No Emulator Bin")); + return; + } this.activeGame = { process: game, diff --git a/src/bun/api/plugins/builtin/emulators/com.simeonradivoev.gameflow.ppsspp/linux/controls.ini b/src/bun/api/plugins/builtin/emulators/com.simeonradivoev.gameflow.ppsspp/linux/controls.ini new file mode 100644 index 0000000..607f58d --- /dev/null +++ b/src/bun/api/plugins/builtin/emulators/com.simeonradivoev.gameflow.ppsspp/linux/controls.ini @@ -0,0 +1,27 @@ +[ControlMapping] +Up = 10-19 +Down = 10-20 +Left = 10-21 +Right = 10-22 +Circle = 10-190 +Cross = 10-189 +Square = 10-191 +Triangle = 10-188 +Start = 10-197 +Select = 10-196 +L = 10-193 +R = 10-192 +An.Up = 10-4003 +An.Down = 10-4002 +An.Left = 10-4001 +An.Right = 10-4000 +Fast-forward = 1-193:10-4010,1-135 +Rewind = 10-196:10-4008 +Save State = 10-196:10-192,1-132 +Load State = 10-196:10-193,1-133 +Previous Slot = 10-197:10-193,1-137 +Next Slot = 10-197:10-192,1-136 +Pause = 10-196:10-107,1-111 +Screenshot = 10-196:10-190 +Exit App = 10-196:10-197 +SpeedToggle = 10-196:10-4010 \ No newline at end of file diff --git a/src/bun/api/plugins/builtin/emulators/com.simeonradivoev.gameflow.ppsspp/linux/ppsspp.ini b/src/bun/api/plugins/builtin/emulators/com.simeonradivoev.gameflow.ppsspp/linux/ppsspp.ini new file mode 100644 index 0000000..edd196b --- /dev/null +++ b/src/bun/api/plugins/builtin/emulators/com.simeonradivoev.gameflow.ppsspp/linux/ppsspp.ini @@ -0,0 +1,479 @@ +[General] +FirstRun = False +RunCount = 4 +Enable Logging = True +AutoRun = True +Browse = False +IgnoreBadMemAccess = True +CurrentDirectory = /home +ShowDebuggerOnLoad = False +CheckForNewVersion = True +Language = en_US +ForceLagSync2 = False +DiscordPresence = True +UISound = False +AutoLoadSaveState = 0 +EnableCheats = True +CwCheatRefreshRate = 77 +CwCheatScrollPosition = 0.000000 +GameListScrollPosition = 0.000000 +ScreenshotsAsPNG = False +UseFFV1 = False +DumpFrames = False +DumpVideoOutput = False +DumpAudio = False +SaveLoadResetsAVdumping = False +StateSlot = 0 +EnableStateUndo = True +StateLoadUndoGame = NA +StateUndoLastSaveGame = NA +StateUndoLastSaveSlot = -5 +RewindFlipFrequency = 0 +ShowOnScreenMessage = True +ShowRegionOnGameIcon = False +ShowIDOnGameIcon = True +GameGridScale = 1.000000 +GridView1 = True +GridView2 = False +GridView3 = False +RightAnalogUp = 0 +RightAnalogDown = 0 +RightAnalogLeft = 0 +RightAnalogRight = 0 +RightAnalogPress = 0 +RightAnalogCustom = False +RightAnalogDisableDiagonal = False +SwipeUp = 0 +SwipeDown = 0 +SwipeLeft = 0 +SwipeRight = 0 +SwipeSensitivity = 1.000000 +SwipeSmoothing = 0.300000 +DoubleTapGesture = 0 +GestureControlEnabled = False +ReportingHost = default +AutoSaveSymbolMap = False +CacheFullIsoInRam = False +RemoteISOPort = 0 +LastRemoteISOServer = +LastRemoteISOPort = 0 +RemoteISOManualConfig = False +RemoteShareOnStartup = False +RemoteISOSubdir = / +RemoteDebuggerOnStartup = False +InternalScreenRotation = 1 +BackgroundAnimation = 1 +PauseWhenMinimized = False +DumpDecryptedEboots = False +MemStickInserted = True +EnablePlugins = True +[CPU] +CPUCore = 1 +SeparateSASThread = True +SeparateIOThread = True +IOTimingMethod = 0 +FastMemoryAccess = True +FunctionReplacements = True +HideSlowWarnings = False +HideStateWarnings = False +PreloadFunctions = False +JitDisableFlags = 0x00000000 +CPUSpeed = 0 +[Graphics] +EnableCardboardVR = False +CardboardScreenSize = 50 +CardboardXShift = 0 +CardboardYShift = 0 +ShowFPSCounter = 0 +GraphicsBackend = 3 (VULKAN) +FailedGraphicsBackends = +DisabledGraphicsBackends = +VulkanDevice = +CameraDevice = +RenderingMode = 1 +SoftwareRenderer = False +HardwareTransform = True +SoftwareSkinning = True +TextureFiltering = 1 +BufferFiltering = 1 +InternalResolution = 3 +AndroidHwScale = 1 +HighQualityDepth = 1 +FrameSkip = 0 +FrameSkipType = 0 +AutoFrameSkip = False +FrameRate = 0 +FrameRate2 = -1 +UnthrottlingMode = CONTINUOUS +AnisotropyLevel = 4 +VertexDecCache = False +TextureBackoffCache = False +TextureSecondaryCache = False +FullScreen = True +FullScreenMulti = False +SmallDisplayZoomType = 2 +SmallDisplayOffsetX = 0.500000 +SmallDisplayOffsetY = 0.500000 +SmallDisplayZoomLevel = 1.000000 +ImmersiveMode = True +SustainedPerformanceMode = False +IgnoreScreenInsets = True +ReplaceTextures = True +SaveNewTextures = False +IgnoreTextureFilenames = False +TexScalingLevel = 1 +TexScalingType = 0 +TexDeposterize = False +TexHardwareScaling = False +VSyncInterval = False +BloomHack = 0 +SplineBezierQuality = 2 +HardwareTessellation = False +TextureShader = Off +ShaderChainRequires60FPS = False +MemBlockTransferGPU = True +DisableSlowFramebufEffects = False +FragmentTestCache = True +LogFrameDrops = False +InflightFrames = 2 +RenderDuplicateFrames = False +[Sound] +Enable = True +AudioBackend = 0 +ExtraAudioBuffering = False +GlobalVolume = 10 +ReverbVolume = 10 +AltSpeedVolume = -1 +AudioDevice = +AutoAudioDevice = False +[Control] +HapticFeedback = False +ShowTouchCross = True +ShowTouchCircle = True +ShowTouchSquare = True +ShowTouchTriangle = True +Custom0Mapping = 0x0000000000000000 +Custom0Image = 0 +Custom0Shape = 0 +Custom0Toggle = False +Custom1Mapping = 0x0000000000000000 +Custom1Image = 1 +Custom1Shape = 0 +Custom1Toggle = False +Custom2Mapping = 0x0000000000000000 +Custom2Image = 2 +Custom2Shape = 0 +Custom2Toggle = False +Custom3Mapping = 0x0000000000000000 +Custom3Image = 3 +Custom3Shape = 0 +Custom3Toggle = False +Custom4Mapping = 0x0000000000000000 +Custom4Image = 4 +Custom4Shape = 0 +Custom4Toggle = False +Custom5Mapping = 0x0000000000000000 +Custom5Image = 0 +Custom5Shape = 1 +Custom5Toggle = False +Custom6Mapping = 0x0000000000000000 +Custom6Image = 1 +Custom6Shape = 1 +Custom6Toggle = False +Custom7Mapping = 0x0000000000000000 +Custom7Image = 2 +Custom7Shape = 1 +Custom7Toggle = False +Custom8Mapping = 0x0000000000000000 +Custom8Image = 3 +Custom8Shape = 1 +Custom8Toggle = False +Custom9Mapping = 0x0000000000000000 +Custom9Image = 4 +Custom9Shape = 1 +Custom9Toggle = False +ShowTouchPause = False +ShowTouchControls = False +DisableDpadDiagonals = False +GamepadOnlyFocused = False +TouchButtonStyle = 1 +TouchButtonOpacity = 65 +TouchButtonHideSeconds = 20 +AutoCenterTouchAnalog = False +AnalogAutoRotSpeed = 8.000000 +TouchSnapToGrid = False +TouchSnapGridSize = 64 +ActionButtonSpacing2 = 1.000000 +ActionButtonCenterX = -1.000000 +ActionButtonCenterY = -1.000000 +ActionButtonScale = 1.150000 +DPadX = -1.000000 +DPadY = -1.000000 +DPadScale = 1.150000 +ShowTouchDpad = True +DPadSpacing = 1.000000 +StartKeyX = -1.000000 +StartKeyY = -1.000000 +StartKeyScale = 1.150000 +ShowTouchStart = True +SelectKeyX = -1.000000 +SelectKeyY = -1.000000 +SelectKeyScale = 1.150000 +ShowTouchSelect = True +UnthrottleKeyX = -1.000000 +UnthrottleKeyY = -1.000000 +UnthrottleKeyScale = 1.150000 +ShowTouchUnthrottle = True +LKeyX = -1.000000 +LKeyY = -1.000000 +LKeyScale = 1.150000 +ShowTouchLTrigger = True +RKeyX = -1.000000 +RKeyY = -1.000000 +RKeyScale = 1.150000 +ShowTouchRTrigger = True +AnalogStickX = -1.000000 +AnalogStickY = -1.000000 +AnalogStickScale = 1.150000 +ShowAnalogStick = True +RightAnalogStickX = -1.000000 +RightAnalogStickY = -1.000000 +RightAnalogStickScale = 1.150000 +ShowRightAnalogStick = False +fcombo0X = -1.000000 +fcombo0Y = -1.000000 +comboKeyScale0 = 1.150000 +ShowComboKey0 = False +fcombo1X = -1.000000 +fcombo1Y = -1.000000 +comboKeyScale1 = 1.150000 +ShowComboKey1 = False +fcombo2X = -1.000000 +fcombo2Y = -1.000000 +comboKeyScale2 = 1.150000 +ShowComboKey2 = False +fcombo3X = -1.000000 +fcombo3Y = -1.000000 +comboKeyScale3 = 1.150000 +ShowComboKey3 = False +fcombo4X = -1.000000 +fcombo4Y = -1.000000 +comboKeyScale4 = 1.150000 +ShowComboKey4 = False +fcombo5X = -1.000000 +fcombo5Y = -1.000000 +comboKeyScale5 = 1.150000 +ShowComboKey5 = False +fcombo6X = -1.000000 +fcombo6Y = -1.000000 +comboKeyScale6 = 1.150000 +ShowComboKey6 = False +fcombo7X = -1.000000 +fcombo7Y = -1.000000 +comboKeyScale7 = 1.150000 +ShowComboKey7 = False +fcombo8X = -1.000000 +fcombo8Y = -1.000000 +comboKeyScale8 = 1.150000 +ShowComboKey8 = False +fcombo9X = -1.000000 +fcombo9Y = -1.000000 +comboKeyScale9 = 1.150000 +ShowComboKey9 = False +AnalogDeadzone = 0.150000 +AnalogInverseDeadzone = 0.000000 +AnalogSensitivity = 1.100000 +AnalogIsCircular = False +AnalogLimiterDeadzone = 0.600000 +LeftStickHeadScale = 1.000000 +RightStickHeadScale = 1.000000 +HideStickBackground = False +UseMouse = False +MapMouse = False +ConfineMap = False +MouseSensitivity = 0.100000 +MouseSmoothing = 0.900000 +SystemControls = True +AllowMappingCombos = True +[Network] +EnableWlan = False +EnableAdhocServer = False +proAdhocServer = socom.cc +PortOffset = 10000 +MinTimeout = 0 +ForcedFirstConnect = False +EnableUPnP = False +UPnPUseOriginalPort = False +EnableNetworkChat = False +ChatButtonPosition = 0 +ChatScreenPosition = 0 +EnableQuickChat = True +QuickChat1 = Quick Chat 1 +QuickChat2 = Quick Chat 2 +QuickChat3 = Quick Chat 3 +QuickChat4 = Quick Chat 4 +QuickChat5 = Quick Chat 5 +[SystemParam] +PSPModel = 1 +PSPFirmwareVersion = 660 +NickName = PPSSPP +MacAddress = ec:fd:62:d4:ec:73 +Language = 1 +ParamTimeFormat = 0 +ParamDateFormat = 0 +TimeZone = 0 +DayLightSavings = False +ButtonPreference = 1 +LockParentalLevel = 0 +WlanAdhocChannel = 0 +WlanPowerSave = False +EncryptSave = True +SavedataUpgradeVersion = True +MemStickSize = 16 +[Debugger] +DisasmWindowX = -1 +DisasmWindowY = -1 +DisasmWindowW = -1 +DisasmWindowH = -1 +GEWindowX = -1 +GEWindowY = -1 +GEWindowW = -1 +GEWindowH = -1 +ConsoleWindowX = -1 +ConsoleWindowY = -1 +FontWidth = 8 +FontHeight = 12 +DisplayStatusBar = True +ShowBottomTabTitles = True +ShowDeveloperMenu = False +SkipDeadbeefFilling = False +FuncHashMap = False +MemInfoDetailed = False +DrawFrameGraph = False +[Upgrade] +UpgradeMessage = +UpgradeVersion = +DismissedVersion = +[Theme] +ItemStyleFg = 0xffffffff +ItemStyleBg = 0x55000000 +ItemFocusedStyleFg = 0xffffffff +ItemFocusedStyleBg = 0xffedc24c +ItemDownStyleFg = 0xffffffff +ItemDownStyleBg = 0xffbd9939 +ItemDisabledStyleFg = 0x80eeeeee +ItemDisabledStyleBg = 0x55e0d4af +ItemHighlightedStyleFg = 0xffffffff +ItemHighlightedStyleBg = 0x55bdbb39 +ButtonStyleFg = 0xffffffff +ButtonStyleBg = 0x55000000 +ButtonFocusedStyleFg = 0xffffffff +ButtonFocusedStyleBg = 0xffedc24c +ButtonDownStyleFg = 0xffffffff +ButtonDownStyleBg = 0xffbd9939 +ButtonDisabledStyleFg = 0x80eeeeee +ButtonDisabledStyleBg = 0x55e0d4af +ButtonHighlightedStyleFg = 0xffffffff +ButtonHighlightedStyleBg = 0x55bdbb39 +HeaderStyleFg = 0xffffffff +InfoStyleFg = 0xffffffff +InfoStyleBg = 0x00000000 +PopupTitleStyleFg = 0xffe3be59 +PopupStyleFg = 0xffffffff +PopupStyleBg = 0xff303030 +[Recent] +MaxRecent = 60 +[Log] +SYSTEMEnabled = True +SYSTEMLevel = 2 +BOOTEnabled = True +BOOTLevel = 2 +COMMONEnabled = True +COMMONLevel = 2 +CPUEnabled = True +CPULevel = 2 +FILESYSEnabled = True +FILESYSLevel = 2 +G3DEnabled = True +G3DLevel = 2 +HLEEnabled = True +HLELevel = 2 +JITEnabled = True +JITLevel = 2 +LOADEREnabled = True +LOADERLevel = 2 +MEEnabled = True +MELevel = 2 +MEMMAPEnabled = True +MEMMAPLevel = 2 +SASMIXEnabled = True +SASMIXLevel = 2 +SAVESTATEEnabled = True +SAVESTATELevel = 2 +FRAMEBUFEnabled = True +FRAMEBUFLevel = 2 +AUDIOEnabled = True +AUDIOLevel = 2 +IOEnabled = True +IOLevel = 2 +SCEAUDIOEnabled = True +SCEAUDIOLevel = 2 +SCECTRLEnabled = True +SCECTRLLevel = 2 +SCEDISPEnabled = True +SCEDISPLevel = 2 +SCEFONTEnabled = True +SCEFONTLevel = 2 +SCEGEEnabled = True +SCEGELevel = 2 +SCEINTCEnabled = True +SCEINTCLevel = 2 +SCEIOEnabled = True +SCEIOLevel = 2 +SCEKERNELEnabled = True +SCEKERNELLevel = 2 +SCEMODULEEnabled = True +SCEMODULELevel = 2 +SCENETEnabled = True +SCENETLevel = 2 +SCERTCEnabled = True +SCERTCLevel = 2 +SCESASEnabled = True +SCESASLevel = 2 +SCEUTILEnabled = True +SCEUTILLevel = 2 +SCEMISCEnabled = True +SCEMISCLevel = 2 +ACHIEVEMENTSEnabled = True +ACHIEVEMENTSLevel = 2 +HTTPEnabled = True +HTTPLevel = 2 +PRINTFEnabled = True +PRINTFLevel = 2 +[PostShaderSetting] +BloomSettingValue1 = 0.600000 +BloomSettingValue2 = 0.500000 +CartoonSettingValue1 = 0.500000 +ColorCorrectionSettingValue1 = 1.000000 +ColorCorrectionSettingValue2 = 1.000000 +ColorCorrectionSettingValue3 = 1.000000 +ColorCorrectionSettingValue4 = 1.000000 +ScanlinesSettingValue1 = 1.000000 +ScanlinesSettingValue2 = 0.500000 +SharpenSettingValue1 = 1.500000 +[Achievements] +AchievementsEnable = False +AchievementsChallengeMode = False +AchievementsEncoreMode = False +AchievementsUnofficial = False +AchievementsLogBadMemReads = False +AchievementsUserName = +AchievementsSoundEffects = True +AchievementsUnlockAudioFile = +AchievementsLeaderboardSubmitAudioFile = +AchievementsLeaderboardTrackerPos = 3 +AchievementsLeaderboardStartedOrFailedPos = 3 +AchievementsLeaderboardSubmittedPos = 3 +AchievementsProgressPos = 3 +AchievementsChallengePos = 3 +AchievementsUnlockedPos = 4 \ No newline at end of file diff --git a/src/bun/api/plugins/builtin/emulators/com.simeonradivoev.gameflow.ppsspp/package.json b/src/bun/api/plugins/builtin/emulators/com.simeonradivoev.gameflow.ppsspp/package.json new file mode 100644 index 0000000..f8e00f5 --- /dev/null +++ b/src/bun/api/plugins/builtin/emulators/com.simeonradivoev.gameflow.ppsspp/package.json @@ -0,0 +1,14 @@ +{ + "name": "com.simeonradivoev.gameflow.ppsspp", + "displayName": "PPSSPP Integration", + "version": "0.0.1", + "description": "PPSSPP Emulator Integration", + "main": "./ppsspp.ts", + "icon": "https://www.ppsspp.org/static/img/platform/ppsspp-icon.png", + "keywords": [ + "integration", + "emulator", + "psp", + "ppsspp" + ] +} \ No newline at end of file diff --git a/src/bun/api/plugins/builtin/emulators/com.simeonradivoev.gameflow.ppsspp/ppsspp.ts b/src/bun/api/plugins/builtin/emulators/com.simeonradivoev.gameflow.ppsspp/ppsspp.ts new file mode 100644 index 0000000..a963cd1 --- /dev/null +++ b/src/bun/api/plugins/builtin/emulators/com.simeonradivoev.gameflow.ppsspp/ppsspp.ts @@ -0,0 +1,54 @@ +import { PluginContextType, PluginType } from "@/bun/types/typesc.schema"; +import desc from './package.json'; +import { config } from "@/bun/api/app"; +import configFilePathWin32 from './win32/ppsspp.ini' with { type: 'file' }; +import configControlsFilePathWin32 from './win32/controls.ini' with { type: 'file' }; +import configFilePathLinux from './linux/ppsspp.ini' with { type: 'file' }; +import configControlsFilePathLinux from './linux/controls.ini' with { type: 'file' }; +import path from "node:path"; +import Mustache from "mustache"; +import { ensureDir } from "fs-extra"; + +export default class PCSX2Integration implements PluginType +{ + load (ctx: PluginContextType) + { + ctx.hooks.games.emulatorLaunch.tapPromise(desc.name, async (ctx) => + { + if (ctx.autoValidCommand.emulator === 'PPSSPP' && ctx.autoValidCommand.emulatorSource === 'store' && ctx.autoValidCommand.metadata.emulatorDir) + { + const args = [ctx.autoValidCommand.metadata.romPath, "--escape-exit", "--pause-menu-exit"]; + if (config.get('launchInFullscreen')) + { + args.push("--fullscreen"); + } + + let confPath: string | undefined = undefined; + let controlsPath: string | undefined = undefined; + + switch (process.platform) + { + case "win32": + confPath = configFilePathWin32; + controlsPath = configControlsFilePathWin32; + break; + case 'linux': + confPath = configFilePathLinux; + controlsPath = configControlsFilePathLinux; + break; + } + + if (controlsPath) + { + const configFileContents = await Bun.file(controlsPath).text(); + const controlsFileContents = await Bun.file(controlsPath).text(); + ensureDir(path.join(ctx.autoValidCommand.metadata.emulatorDir, 'memstick', 'PSP', 'SYSTEM')); + await Bun.write(path.join(ctx.autoValidCommand.metadata.emulatorDir, 'memstick', 'PSP', 'SYSTEM', 'ppsspp.ini'), Mustache.render(configFileContents, {})); + await Bun.write(path.join(ctx.autoValidCommand.metadata.emulatorDir, 'memstick', 'PSP', 'SYSTEM', 'controls.ini'), Mustache.render(controlsFileContents, {})); + } + + return args; + } + }); + } +} \ No newline at end of file diff --git a/src/bun/api/plugins/builtin/emulators/com.simeonradivoev.gameflow.ppsspp/win32/controls.ini b/src/bun/api/plugins/builtin/emulators/com.simeonradivoev.gameflow.ppsspp/win32/controls.ini new file mode 100644 index 0000000..54afdfa --- /dev/null +++ b/src/bun/api/plugins/builtin/emulators/com.simeonradivoev.gameflow.ppsspp/win32/controls.ini @@ -0,0 +1,23 @@ +[ControlMapping] +Up = 20-19 +Down = 20-20 +Left = 20-21 +Right = 20-22 +Circle = 20-97 +Cross = 20-96 +Square = 20-100 +Triangle = 20-99 +Start = 20-108 +Select = 20-109 +L = 20-102 +R = 20-103 +An.Up = 20-4002 +An.Down = 20-4003 +An.Left = 20-4001 +An.Right = 20-4000 +Fast-forward = 20-109:20-4036 +Rewind = 20-109:20-4034 +Save State = 20-109:20-103 +Load State = 20-109:20-102 +Home = 20-108:20-109 +Exit App = 20-3:20-108 \ No newline at end of file diff --git a/src/bun/api/plugins/builtin/emulators/com.simeonradivoev.gameflow.ppsspp/win32/ppsspp.ini b/src/bun/api/plugins/builtin/emulators/com.simeonradivoev.gameflow.ppsspp/win32/ppsspp.ini new file mode 100644 index 0000000..f24ea4b --- /dev/null +++ b/src/bun/api/plugins/builtin/emulators/com.simeonradivoev.gameflow.ppsspp/win32/ppsspp.ini @@ -0,0 +1,472 @@ +[General] +FirstRun = False +RunCount = 4 +Enable Logging = True +AutoRun = True +Browse = False +IgnoreBadMemAccess = True +CurrentDirectory = C:/Emulation/roms/psp +ShowDebuggerOnLoad = False +CheckForNewVersion = True +Language = en_US +ForceLagSync2 = False +DiscordPresence = True +UISound = False +AutoLoadSaveState = 0 +EnableCheats = True +CwCheatRefreshRate = 77 +CwCheatScrollPosition = 0.000000 +GameListScrollPosition = 0.000000 +ScreenshotsAsPNG = False +UseFFV1 = False +DumpFrames = False +DumpVideoOutput = False +DumpAudio = False +SaveLoadResetsAVdumping = False +StateSlot = 0 +EnableStateUndo = True +StateLoadUndoGame = NA +StateUndoLastSaveGame = NA +StateUndoLastSaveSlot = -5 +RewindFlipFrequency = 0 +ShowOnScreenMessage = True +ShowRegionOnGameIcon = False +ShowIDOnGameIcon = False +GameGridScale = 1.000000 +GridView1 = True +GridView2 = True +GridView3 = False +RightAnalogUp = 0 +RightAnalogDown = 0 +RightAnalogLeft = 0 +RightAnalogRight = 0 +RightAnalogPress = 0 +RightAnalogCustom = False +RightAnalogDisableDiagonal = False +SwipeUp = 0 +SwipeDown = 0 +SwipeLeft = 0 +SwipeRight = 0 +SwipeSensitivity = 1.000000 +SwipeSmoothing = 0.300000 +DoubleTapGesture = 0 +GestureControlEnabled = False +ReportingHost = default +AutoSaveSymbolMap = False +CacheFullIsoInRam = False +RemoteISOPort = 0 +LastRemoteISOServer = +LastRemoteISOPort = 0 +RemoteISOManualConfig = False +RemoteShareOnStartup = False +RemoteISOSubdir = / +RemoteDebuggerOnStartup = False +InternalScreenRotation = 1 +BackgroundAnimation = 1 +PauseWhenMinimized = False +DumpDecryptedEboots = False +MemStickInserted = True +EnablePlugins = True +[CPU] +CPUCore = 1 +SeparateSASThread = True +SeparateIOThread = True +IOTimingMethod = 0 +FastMemoryAccess = True +FunctionReplacements = True +HideSlowWarnings = False +HideStateWarnings = False +PreloadFunctions = False +JitDisableFlags = 0x00000000 +CPUSpeed = 0 +[Graphics] +EnableCardboardVR = False +CardboardScreenSize = 50 +CardboardXShift = 0 +CardboardYShift = 0 +ShowFPSCounter = 0 +GraphicsBackend = 3 (VULKAN) +FailedGraphicsBackends = +DisabledGraphicsBackends = +VulkanDevice = +CameraDevice = +RenderingMode = 1 +SoftwareRenderer = False +HardwareTransform = True +SoftwareSkinning = True +TextureFiltering = 1 +BufferFiltering = 1 +InternalResolution = 3 +AndroidHwScale = 1 +HighQualityDepth = 1 +FrameSkip = 0 +FrameSkipType = 0 +AutoFrameSkip = False +FrameRate = 0 +FrameRate2 = -1 +UnthrottlingMode = CONTINUOUS +AnisotropyLevel = 4 +VertexDecCache = False +TextureBackoffCache = False +TextureSecondaryCache = False +FullScreen = True +FullScreenMulti = False +SmallDisplayZoomType = 2 +SmallDisplayOffsetX = 0.500000 +SmallDisplayOffsetY = 0.500000 +SmallDisplayZoomLevel = 1.000000 +ImmersiveMode = True +SustainedPerformanceMode = False +IgnoreScreenInsets = True +ReplaceTextures = True +SaveNewTextures = False +IgnoreTextureFilenames = False +TexScalingLevel = 1 +TexScalingType = 0 +TexDeposterize = False +TexHardwareScaling = False +VSyncInterval = False +BloomHack = 0 +SplineBezierQuality = 2 +HardwareTessellation = False +TextureShader = Off +ShaderChainRequires60FPS = False +MemBlockTransferGPU = True +DisableSlowFramebufEffects = False +FragmentTestCache = True +LogFrameDrops = False +InflightFrames = 2 +RenderDuplicateFrames = False +[Sound] +Enable = True +AudioBackend = 0 +ExtraAudioBuffering = False +GlobalVolume = 10 +ReverbVolume = 10 +AltSpeedVolume = -1 +AudioDevice = +AutoAudioDevice = False +[Control] +HapticFeedback = False +ShowTouchCross = True +ShowTouchCircle = True +ShowTouchSquare = True +ShowTouchTriangle = True +Custom0Mapping = 0x0000000000000000 +Custom0Image = 0 +Custom0Shape = 0 +Custom0Toggle = False +Custom1Mapping = 0x0000000000000000 +Custom1Image = 1 +Custom1Shape = 0 +Custom1Toggle = False +Custom2Mapping = 0x0000000000000000 +Custom2Image = 2 +Custom2Shape = 0 +Custom2Toggle = False +Custom3Mapping = 0x0000000000000000 +Custom3Image = 3 +Custom3Shape = 0 +Custom3Toggle = False +Custom4Mapping = 0x0000000000000000 +Custom4Image = 4 +Custom4Shape = 0 +Custom4Toggle = False +Custom5Mapping = 0x0000000000000000 +Custom5Image = 0 +Custom5Shape = 1 +Custom5Toggle = False +Custom6Mapping = 0x0000000000000000 +Custom6Image = 1 +Custom6Shape = 1 +Custom6Toggle = False +Custom7Mapping = 0x0000000000000000 +Custom7Image = 2 +Custom7Shape = 1 +Custom7Toggle = False +Custom8Mapping = 0x0000000000000000 +Custom8Image = 3 +Custom8Shape = 1 +Custom8Toggle = False +Custom9Mapping = 0x0000000000000000 +Custom9Image = 4 +Custom9Shape = 1 +Custom9Toggle = False +ShowTouchPause = False +ShowTouchControls = False +DisableDpadDiagonals = False +GamepadOnlyFocused = False +TouchButtonStyle = 1 +TouchButtonOpacity = 65 +TouchButtonHideSeconds = 20 +AutoCenterTouchAnalog = False +AnalogAutoRotSpeed = 8.000000 +TouchSnapToGrid = False +TouchSnapGridSize = 64 +ActionButtonSpacing2 = 1.000000 +ActionButtonCenterX = -1.000000 +ActionButtonCenterY = -1.000000 +ActionButtonScale = 1.150000 +DPadX = -1.000000 +DPadY = -1.000000 +DPadScale = 1.150000 +ShowTouchDpad = True +DPadSpacing = 1.000000 +StartKeyX = -1.000000 +StartKeyY = -1.000000 +StartKeyScale = 1.150000 +ShowTouchStart = True +SelectKeyX = -1.000000 +SelectKeyY = -1.000000 +SelectKeyScale = 1.150000 +ShowTouchSelect = True +UnthrottleKeyX = -1.000000 +UnthrottleKeyY = -1.000000 +UnthrottleKeyScale = 1.150000 +ShowTouchUnthrottle = True +LKeyX = -1.000000 +LKeyY = -1.000000 +LKeyScale = 1.150000 +ShowTouchLTrigger = True +RKeyX = -1.000000 +RKeyY = -1.000000 +RKeyScale = 1.150000 +ShowTouchRTrigger = True +AnalogStickX = -1.000000 +AnalogStickY = -1.000000 +AnalogStickScale = 1.150000 +ShowAnalogStick = True +RightAnalogStickX = -1.000000 +RightAnalogStickY = -1.000000 +RightAnalogStickScale = 1.150000 +ShowRightAnalogStick = False +fcombo0X = -1.000000 +fcombo0Y = -1.000000 +comboKeyScale0 = 1.150000 +ShowComboKey0 = False +fcombo1X = -1.000000 +fcombo1Y = -1.000000 +comboKeyScale1 = 1.150000 +ShowComboKey1 = False +fcombo2X = -1.000000 +fcombo2Y = -1.000000 +comboKeyScale2 = 1.150000 +ShowComboKey2 = False +fcombo3X = -1.000000 +fcombo3Y = -1.000000 +comboKeyScale3 = 1.150000 +ShowComboKey3 = False +fcombo4X = -1.000000 +fcombo4Y = -1.000000 +comboKeyScale4 = 1.150000 +ShowComboKey4 = False +fcombo5X = -1.000000 +fcombo5Y = -1.000000 +comboKeyScale5 = 1.150000 +ShowComboKey5 = False +fcombo6X = -1.000000 +fcombo6Y = -1.000000 +comboKeyScale6 = 1.150000 +ShowComboKey6 = False +fcombo7X = -1.000000 +fcombo7Y = -1.000000 +comboKeyScale7 = 1.150000 +ShowComboKey7 = False +fcombo8X = -1.000000 +fcombo8Y = -1.000000 +comboKeyScale8 = 1.150000 +ShowComboKey8 = False +fcombo9X = -1.000000 +fcombo9Y = -1.000000 +comboKeyScale9 = 1.150000 +ShowComboKey9 = False +AnalogDeadzone = 0.150000 +AnalogInverseDeadzone = 0.000000 +AnalogSensitivity = 1.100000 +AnalogIsCircular = False +AnalogLimiterDeadzone = 0.600000 +LeftStickHeadScale = 1.000000 +RightStickHeadScale = 1.000000 +HideStickBackground = False +UseMouse = False +MapMouse = False +ConfineMap = False +MouseSensitivity = 0.100000 +MouseSmoothing = 0.900000 +SystemControls = True +AllowMappingCombos = True +[Network] +EnableWlan = False +EnableAdhocServer = False +proAdhocServer = socom.cc +PortOffset = 10000 +MinTimeout = 0 +ForcedFirstConnect = False +EnableUPnP = False +UPnPUseOriginalPort = False +EnableNetworkChat = False +ChatButtonPosition = 0 +ChatScreenPosition = 0 +EnableQuickChat = True +QuickChat1 = Quick Chat 1 +QuickChat2 = Quick Chat 2 +QuickChat3 = Quick Chat 3 +QuickChat4 = Quick Chat 4 +QuickChat5 = Quick Chat 5 +[SystemParam] +PSPModel = 1 +PSPFirmwareVersion = 660 +NickName = PPSSPP +MacAddress = ec:fd:62:d4:ec:73 +Language = 1 +ParamTimeFormat = 0 +ParamDateFormat = 0 +TimeZone = 0 +DayLightSavings = False +ButtonPreference = 1 +LockParentalLevel = 0 +WlanAdhocChannel = 0 +WlanPowerSave = False +EncryptSave = True +SavedataUpgradeVersion = True +MemStickSize = 16 +[Debugger] +DisasmWindowX = -1 +DisasmWindowY = -1 +DisasmWindowW = -1 +DisasmWindowH = -1 +GEWindowX = -1 +GEWindowY = -1 +GEWindowW = -1 +GEWindowH = -1 +ConsoleWindowX = -1 +ConsoleWindowY = -1 +FontWidth = 8 +FontHeight = 12 +DisplayStatusBar = True +ShowBottomTabTitles = True +ShowDeveloperMenu = False +SkipDeadbeefFilling = False +FuncHashMap = False +MemInfoDetailed = False +DrawFrameGraph = False +[Upgrade] +UpgradeMessage = +UpgradeVersion = +DismissedVersion = +[Theme] +ItemStyleFg = 0xffffffff +ItemStyleBg = 0x55000000 +ItemFocusedStyleFg = 0xffffffff +ItemFocusedStyleBg = 0xffedc24c +ItemDownStyleFg = 0xffffffff +ItemDownStyleBg = 0xffbd9939 +ItemDisabledStyleFg = 0x80eeeeee +ItemDisabledStyleBg = 0x55e0d4af +ItemHighlightedStyleFg = 0xffffffff +ItemHighlightedStyleBg = 0x55bdbb39 +ButtonStyleFg = 0xffffffff +ButtonStyleBg = 0x55000000 +ButtonFocusedStyleFg = 0xffffffff +ButtonFocusedStyleBg = 0xffedc24c +ButtonDownStyleFg = 0xffffffff +ButtonDownStyleBg = 0xffbd9939 +ButtonDisabledStyleFg = 0x80eeeeee +ButtonDisabledStyleBg = 0x55e0d4af +ButtonHighlightedStyleFg = 0xffffffff +ButtonHighlightedStyleBg = 0x55bdbb39 +HeaderStyleFg = 0xffffffff +InfoStyleFg = 0xffffffff +InfoStyleBg = 0x00000000 +PopupTitleStyleFg = 0xffe3be59 +PopupStyleFg = 0xffffffff +PopupStyleBg = 0xff303030 +[Recent] +MaxRecent = 60 +[Log] +SYSTEMEnabled = True +SYSTEMLevel = 2 +BOOTEnabled = True +BOOTLevel = 2 +COMMONEnabled = True +COMMONLevel = 2 +CPUEnabled = True +CPULevel = 2 +FILESYSEnabled = True +FILESYSLevel = 2 +G3DEnabled = True +G3DLevel = 2 +HLEEnabled = True +HLELevel = 2 +JITEnabled = True +JITLevel = 2 +LOADEREnabled = True +LOADERLevel = 2 +MEEnabled = True +MELevel = 2 +MEMMAPEnabled = True +MEMMAPLevel = 2 +SASMIXEnabled = True +SASMIXLevel = 2 +SAVESTATEEnabled = True +SAVESTATELevel = 2 +FRAMEBUFEnabled = True +FRAMEBUFLevel = 2 +AUDIOEnabled = True +AUDIOLevel = 2 +IOEnabled = True +IOLevel = 2 +SCEAUDIOEnabled = True +SCEAUDIOLevel = 2 +SCECTRLEnabled = True +SCECTRLLevel = 2 +SCEDISPEnabled = True +SCEDISPLevel = 2 +SCEFONTEnabled = True +SCEFONTLevel = 2 +SCEGEEnabled = True +SCEGELevel = 2 +SCEINTCEnabled = True +SCEINTCLevel = 2 +SCEIOEnabled = True +SCEIOLevel = 2 +SCEKERNELEnabled = True +SCEKERNELLevel = 2 +SCEMODULEEnabled = True +SCEMODULELevel = 2 +SCENETEnabled = True +SCENETLevel = 2 +SCERTCEnabled = True +SCERTCLevel = 2 +SCESASEnabled = True +SCESASLevel = 2 +SCEUTILEnabled = True +SCEUTILLevel = 2 +SCEMISCEnabled = True +SCEMISCLevel = 2 +[PostShaderSetting] +BloomSettingValue1 = 0.600000 +BloomSettingValue2 = 0.500000 +CartoonSettingValue1 = 0.500000 +ColorCorrectionSettingValue1 = 1.000000 +ColorCorrectionSettingValue2 = 1.000000 +ColorCorrectionSettingValue3 = 1.000000 +ColorCorrectionSettingValue4 = 1.000000 +ScanlinesSettingValue1 = 1.000000 +ScanlinesSettingValue2 = 0.500000 +SharpenSettingValue1 = 1.500000 +[Achievements] +AchievementsEnable = False +AchievementsChallengeMode = False +AchievementsEncoreMode = False +AchievementsUnofficial = False +AchievementsLogBadMemReads = False +AchievementsSoundEffects = True +AchievementsUnlockAudioFile = +AchievementsLeaderboardSubmitAudioFile = +AchievementsLeaderboardTrackerPos = 3 +AchievementsLeaderboardStartedOrFailedPos = 3 +AchievementsLeaderboardSubmittedPos = 3 +AchievementsProgressPos = 3 +AchievementsChallengePos = 3 +AchievementsUnlockedPos = 4 \ No newline at end of file diff --git a/src/bun/api/plugins/register-plugins.ts b/src/bun/api/plugins/register-plugins.ts index d3b9667..9f223b2 100644 --- a/src/bun/api/plugins/register-plugins.ts +++ b/src/bun/api/plugins/register-plugins.ts @@ -1,6 +1,7 @@ import { PluginManager } from "./plugin-manager"; import pcsx2 from './builtin/emulators/com.simeonradivoev.gameflow.pcsx2/package.json'; +import ppsspp from './builtin/emulators/com.simeonradivoev.gameflow.ppsspp/package.json'; import romm from './builtin/sources/com.simeonradivoev.gameflow.romm/package.json'; import { PluginDescriptionSchema, PluginDescriptionType, PluginSchema } from "@/bun/types/typesc.schema"; @@ -9,6 +10,7 @@ export default async function register (pluginManager: PluginManager) const plugins: (PluginDescriptionType & { main: string; load: () => Promise; })[] = [ { ...pcsx2, load: () => import('./builtin/emulators/com.simeonradivoev.gameflow.pcsx2/pcsx2') }, + { ...ppsspp, load: () => import('./builtin/emulators/com.simeonradivoev.gameflow.ppsspp/ppsspp') }, { ...romm, load: () => import('./builtin/sources/com.simeonradivoev.gameflow.romm/romm') }, ]; diff --git a/src/bun/api/system.ts b/src/bun/api/system.ts index 8f5eec4..aa9207e 100644 --- a/src/bun/api/system.ts +++ b/src/bun/api/system.ts @@ -62,33 +62,46 @@ export const system = new Elysia({ prefix: '/api/system' }) return new Response(buildNotificationsStream()); }) .ws('/info/system', { - response: SystemInfoSchema, + response: z.discriminatedUnion('type', [ + z.object({ type: z.literal('info'), data: SystemInfoSchema }), + z.object({ type: z.literal('focus') }) + ]), async open (ws) { const battery = await si.battery(); const wifi = await si.wifiConnections(); const bluetooth = await si.bluetoothDevices(); ws.send({ - battery: battery, - wifiConnections: wifi, - bluetoothDevices: bluetooth + type: 'info', + data: { + battery: battery, + wifiConnections: wifi, + bluetoothDevices: bluetooth + } }, true); + const handleFocus = () => ws.send({ type: 'focus' }); + events.on('focus', handleFocus); + (ws.data as any).dispose = [() => events.removeListener('focus', handleFocus)]; (ws.data as any).observer = setInterval(async () => { const battery = await si.battery(); const wifi = await si.wifiConnections(); const bluetooth = await si.bluetoothDevices(); ws.send({ - battery: battery, - wifiConnections: wifi, - bluetoothDevices: bluetooth + type: 'info', + data: { + battery: battery, + wifiConnections: wifi, + bluetoothDevices: bluetooth + } }, true); }, 1000 * 30); }, close (ws) { clearInterval((ws.data as any).observer); + (ws.data as any).dispose.forEach((dispose: any) => dispose()); } }) .get('/drives', async () => diff --git a/src/bun/browser.ts b/src/bun/browser.ts index 1f1acbb..1feb8fd 100644 --- a/src/bun/browser.ts +++ b/src/bun/browser.ts @@ -2,6 +2,7 @@ import { killBrowser, spawnBrowser } from './utils/browser-spawner'; import { BrowserParams, BuildParams } from './utils/browser-params'; import os from 'node:os'; import { EventEmitter } from 'node:stream'; +import { dlopen, FFIType } from "bun:ffi"; export default async function init (events: EventEmitter, forceBrowser: boolean, params: BrowserParams) { @@ -31,8 +32,6 @@ async function runWebview (events: EventEmitter, params: BrowserParams) config.WINDOW_HEIGHT = String(params.windowSize?.height); } const webviewWorker = new Worker(webviewPath, { - smol: true, - ref: false, env: { ...config, ...process.env as any @@ -41,7 +40,6 @@ async function runWebview (events: EventEmitter, params: BrowserParams) return new Promise((resolve, reject) => { - const handleExit = () => { resolve(true); @@ -49,6 +47,8 @@ async function runWebview (events: EventEmitter, params: BrowserParams) webviewWorker.terminate(); }; + let pointer: any = undefined; + webviewWorker.addEventListener('error', e => { console.error(e.message); @@ -64,10 +64,35 @@ async function runWebview (events: EventEmitter, params: BrowserParams) { console.log("Webview Destroyed"); resolve(true); + } else if (e.data.type === 'pointer') + { + pointer = e.data.data; } }); events.on('exitapp', handleExit); + events.on('focus', () => + { + if (process.platform === 'win32') + { + const user32 = dlopen("user32.dll", { + SetForegroundWindow: { args: [FFIType.ptr], returns: FFIType.bool }, + ShowWindow: { args: [FFIType.ptr, FFIType.i32], returns: FFIType.bool }, + BringWindowToTop: { args: [FFIType.ptr], returns: FFIType.bool }, + keybd_event: { args: [FFIType.u8, FFIType.u8, FFIType.u32, FFIType.ptr], returns: FFIType.void }, + }); + + const SW_RESTORE = 9; + + if (pointer) + { + user32.symbols.ShowWindow(pointer, SW_RESTORE); + user32.symbols.keybd_event(0, 0, 0, null); // fake input event + user32.symbols.BringWindowToTop(pointer); + user32.symbols.SetForegroundWindow(pointer); + } + } + }); }); } diff --git a/src/bun/controls.ts b/src/bun/controls.ts new file mode 100644 index 0000000..f88707a --- /dev/null +++ b/src/bun/controls.ts @@ -0,0 +1,53 @@ + + +import { LaunchGameJob } from './api/jobs/launch-game-job'; +import { events, taskQueue } from './api/app'; + +process.env.SDL_JOYSTICK_ALLOW_BACKGROUND_EVENTS = "1"; +process.env.SDL_JOYSTICK_THREAD = "1"; + +export default async function Initialize () +{ + const { default: sdl } = await import('@kmamal/sdl'); + const launcherWin = sdl.video.createWindow({ title: "Launcher", visible: false }); + + sdl.controller.devices.forEach(d => connectToController(d)); + sdl.controller.on('deviceAdd', e => + { + connectToController(e.device); + }); + + function connectToController (device: any) + { + let selectHeld = false; + const ctrl = sdl.controller.openDevice(device); + console.log("Connected to", device.name); + + ctrl.on("buttonDown", ({ button }) => + { + if (button === "back") selectHeld = true; + if (button === "start" && selectHeld) + { + const launchGameTask = taskQueue.findJob(LaunchGameJob.id, LaunchGameJob); + if (launchGameTask) + { + launchGameTask.abort('exit'); + taskQueue.waitForJob(LaunchGameJob.id).then(() => setTimeout(() => events.emit('focus'), 300)); + } else + { + events.emit('focus'); + } + } + + if (button === 'guide') + { + events.emit('focus'); + } + }); + + ctrl.on("buttonUp", ({ button }) => + { + if (button === "back") selectHeld = false; + }); + } +} \ No newline at end of file diff --git a/src/bun/index.ts b/src/bun/index.ts index 476c8a6..5601e11 100644 --- a/src/bun/index.ts +++ b/src/bun/index.ts @@ -38,12 +38,16 @@ if (process.env.HEADLESS) } }); - // Called by user + // Using stdout for communication as ipc doesn't seem to work with dev.ts script app.events.on('exitapp', () => { - process.send?.({ type: 'exitapp' }); + process.stdout.write('exitapp\n'); cleanup(); }); + app.events.on('focus', () => + { + process.stdout.write("focus\n"); + }); } else { await init(app.events, Bun.env.FORCE_BROWSER === "true", { diff --git a/src/bun/types/types.d.ts b/src/bun/types/types.d.ts index b9208d8..1bc7a22 100644 --- a/src/bun/types/types.d.ts +++ b/src/bun/types/types.d.ts @@ -28,4 +28,5 @@ declare interface AppEventMap { exitapp: []; notification: [FrontendNotification]; + focus: []; } \ No newline at end of file diff --git a/src/bun/types/typesc.schema.ts b/src/bun/types/typesc.schema.ts index 09d29e6..fba1435 100644 --- a/src/bun/types/typesc.schema.ts +++ b/src/bun/types/typesc.schema.ts @@ -27,7 +27,7 @@ export type PluginContextType = z.infer; export type PluginDescriptionType = z.infer; export const ActiveGameSchema = z.object({ - process: z.instanceof(ChildProcess).optional(), + process: z.any().optional(), gameId: z.number(), name: z.string(), command: z.object({ command: z.string(), startDir: z.string().optional() }) diff --git a/src/bun/utils/browser-spawner.ts b/src/bun/utils/browser-spawner.ts index 5481580..670c41d 100644 --- a/src/bun/utils/browser-spawner.ts +++ b/src/bun/utils/browser-spawner.ts @@ -1,4 +1,5 @@ import { $, type Subprocess } from "bun"; +import { ChildProcessWithoutNullStreams } from "node:child_process"; import os from 'node:os'; export type RunBrowserType = "chrome" | "chromium" | "firefox" | "edge"; @@ -163,7 +164,7 @@ export async function spawnBrowser ({ return processSub; } -export async function killBrowser (browser: Subprocess) +export async function killBrowser (browser: Subprocess | ChildProcessWithoutNullStreams) { if (os.platform() === 'linux') { diff --git a/src/bun/webview/win32.ts b/src/bun/webview/win32.ts index 6ef0a20..b575f57 100644 --- a/src/bun/webview/win32.ts +++ b/src/bun/webview/win32.ts @@ -6,4 +6,5 @@ let size: Size | undefined = undefined; if (process.env.WINDOW_WIDTH && process.env.WINDOW_HEIGHT) size = { width: Number(process.env.WINDOW_WIDTH), height: Number(process.env.WINDOW_HEIGHT), hint: SizeHint.NONE }; const webview = new Webview(process.env.NODE_ENV === 'development', size); +self.postMessage({ type: 'pointer', data: webview.unsafeWindowHandle }); webviewWorkerBase(webview); \ No newline at end of file diff --git a/src/mainview/components/AppCommunication.tsx b/src/mainview/components/AppCommunication.tsx new file mode 100644 index 0000000..f0ba0a3 --- /dev/null +++ b/src/mainview/components/AppCommunication.tsx @@ -0,0 +1,33 @@ +import { useEffect, useState } from "react"; +import { SystemInfoContext } from "../scripts/contexts"; +import { systemApi } from "../scripts/clientApi"; +import { SystemInfoType } from "@/shared/constants"; + +export default function AppCommunication (data: { children: any; }) +{ + + const [systemInfo, setSystemInfo] = useState(); + useEffect(() => + { + const sub = systemApi.api.system.info.system.subscribe(); + sub.subscribe(({ data }) => + { + switch (data.type) + { + case "info": + setSystemInfo(data.data); + break; + case "focus": + window.focus(); + break; + } + + }); + + document.documentElement.dataset.loaded = "true"; + }, []); + + return + {data.children} + ; +} \ No newline at end of file diff --git a/src/mainview/gen/static-icon-assets.gen.ts b/src/mainview/gen/static-icon-assets.gen.ts index 1d1a4aa..cb3fe1b 100644 --- a/src/mainview/gen/static-icon-assets.gen.ts +++ b/src/mainview/gen/static-icon-assets.gen.ts @@ -464,7 +464,7 @@ const assets = new Set([ ]); // Store basePath resolved from Vite config -const BASE_PATH = "/"; +const BASE_PATH = "./"; /** diff --git a/src/mainview/routes/__root.tsx b/src/mainview/routes/__root.tsx index bdee113..345a707 100644 --- a/src/mainview/routes/__root.tsx +++ b/src/mainview/routes/__root.tsx @@ -8,6 +8,7 @@ import { useEffect, useState } from "react"; import { SystemInfoContext } from "../scripts/contexts"; import { SystemInfoType } from "@/shared/constants"; import { systemApi } from "../scripts/clientApi"; +import AppCommunication from "../components/AppCommunication"; export const Route = createRootRouteWithContext()({ component: RootComponent, @@ -34,23 +35,11 @@ function RootComponent () }, [theme]); - const [systemInfo, setSystemInfo] = useState(); - useEffect(() => - { - const sub = systemApi.api.system.info.system.subscribe(); - sub.subscribe(({ data }) => - { - setSystemInfo(data); - }); - - document.documentElement.dataset.loaded = "true"; - }, []); - return (
- + - + {/*import.meta.env.DEV && !isMobile && diff --git a/src/tests/game-launching.test.ts b/src/tests/game-launching.test.ts index 7618096..04430ab 100644 --- a/src/tests/game-launching.test.ts +++ b/src/tests/game-launching.test.ts @@ -1,19 +1,25 @@ import { expect, test } from 'bun:test'; +import { resolve } from 'node:path'; +import './preload'; test("uses custom emulator", async () => { - const { getValidLaunchCommands: getLaunchCommands } = await import('../bun/api/games/services/launchGameService'); + const { customEmulators } = await import('@/bun/api/app'); + customEmulators.set('PCSX2', resolve("./src/tests/mock-roms/mock-emulator.exe")); + + const { getValidLaunchCommands: getLaunchCommands } = await import('@/bun/api/games/services/launchGameService'); const commands = await getLaunchCommands({ systemSlug: 'ps2', - gamePath: './src/tests/mock-roms/mock-rom.iso', - customEmulatorConfig: new Map([['PCSX2', "./src/tests/mock-roms/pcsx2.exe"]]) + gamePath: './mock-rom.iso' }); expect(commands) .toSatisfy((d) => - !!d?.find(c => - c?.command.includes("./src/tests/mock-roms/mock-rom.iso") && - c.command.includes("./src/tests/mock-roms/pcsx2.exe") - ) - ); + { + const validCommand = d.find(c => + c?.command.includes("mock-rom.iso") && + c.command.includes("mock-emulator.exe") + ); + return !!validCommand; + }); }); \ No newline at end of file diff --git a/src/tests/mock-roms/mock-emulator.exe b/src/tests/mock-roms/mock-emulator.exe new file mode 100644 index 0000000..e69de29 diff --git a/src/tests/preload.ts b/src/tests/preload.ts index 2b3b7cc..83dc4a2 100644 --- a/src/tests/preload.ts +++ b/src/tests/preload.ts @@ -1,2 +1,11 @@ -import { mock } from 'bun:test'; +import { beforeAll } from 'bun:test'; +import { resolve } from 'node:path'; +beforeAll(async () => +{ + process.env.CUSTOM_STORE_PATH = resolve('./src/tests/mock-store'); + process.env.CONFIG_CWD = resolve('./src/tests/mock-config'); + + const { config } = await import('@/bun/api/app'); + config.set('downloadPath', resolve('./src/tests/mock-roms')); +}); \ No newline at end of file