fix: ditched sdl and moved to xinput for windows for less ram usage
This commit is contained in:
parent
90d6711935
commit
dc0f2d150a
18 changed files with 663 additions and 100 deletions
3
.gitignore
vendored
3
.gitignore
vendored
|
|
@ -28,4 +28,5 @@ downloads
|
||||||
gameflow-deck.code-workspace
|
gameflow-deck.code-workspace
|
||||||
.env.local
|
.env.local
|
||||||
src/tests/mock-roms/db.sqlite
|
src/tests/mock-roms/db.sqlite
|
||||||
src/tests/mock-config
|
src/tests/mock-config
|
||||||
|
bin
|
||||||
5
.vscode/tasks.json
vendored
5
.vscode/tasks.json
vendored
|
|
@ -38,11 +38,6 @@
|
||||||
"label": "Start Dev (Hot Reload)",
|
"label": "Start Dev (Hot Reload)",
|
||||||
"type": "shell",
|
"type": "shell",
|
||||||
"command": "bun run dev:hmr",
|
"command": "bun run dev:hmr",
|
||||||
"options": {
|
|
||||||
"env": {
|
|
||||||
"FORCE_BROWSER": "false"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"isBackground": true,
|
"isBackground": true,
|
||||||
"problemMatcher": [],
|
"problemMatcher": [],
|
||||||
"presentation": {
|
"presentation": {
|
||||||
|
|
|
||||||
15
bun.lock
15
bun.lock
|
|
@ -10,7 +10,6 @@
|
||||||
"@elysiajs/cors": "^1.4.1",
|
"@elysiajs/cors": "^1.4.1",
|
||||||
"@elysiajs/eden": "^1.4.6",
|
"@elysiajs/eden": "^1.4.6",
|
||||||
"@jimp/wasm-webp": "^1.6.0",
|
"@jimp/wasm-webp": "^1.6.0",
|
||||||
"@kmamal/sdl": "^0.11.13",
|
|
||||||
"cheerio": "^1.2.0",
|
"cheerio": "^1.2.0",
|
||||||
"conf": "^15.0.2",
|
"conf": "^15.0.2",
|
||||||
"drizzle-orm": "^0.45.1",
|
"drizzle-orm": "^0.45.1",
|
||||||
|
|
@ -327,8 +326,6 @@
|
||||||
|
|
||||||
"@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/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/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=="],
|
"@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=="],
|
||||||
|
|
@ -403,8 +400,6 @@
|
||||||
|
|
||||||
"@jsquash/webp": ["@jsquash/webp@1.5.0", "", { "dependencies": { "wasm-feature-detect": "^1.2.11" } }, "sha512-KggLoj2MnRSfIqTeKe1EmbljTX2vuV7mh79k89PCL1pyqiDULcPM1L47twxXt0hkb68F70bXiL31MxsuoZtKFw=="],
|
"@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/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=="],
|
"@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=="],
|
||||||
|
|
@ -739,8 +734,6 @@
|
||||||
|
|
||||||
"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=="],
|
"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=="],
|
"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=="],
|
"classnames": ["classnames@2.5.1", "", {}, "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow=="],
|
||||||
|
|
@ -1219,8 +1212,6 @@
|
||||||
|
|
||||||
"minipass": ["minipass@7.1.3", "", {}, "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A=="],
|
"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=="],
|
"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=="],
|
"modify-values": ["modify-values@1.0.1", "", {}, "sha512-xV2bxeN6F7oYjZWTe/YPAy6MN2M+sL4u/Rlm2AHCIVGfo2p1yGmBHQ6vHehl4bRTZBdHu3TSkWdYgkwpYzAGSw=="],
|
||||||
|
|
@ -1559,8 +1550,6 @@
|
||||||
|
|
||||||
"tapable": ["tapable@2.3.0", "", {}, "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg=="],
|
"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=="],
|
"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=="],
|
"text-extensions": ["text-extensions@1.9.0", "", {}, "sha512-wiBrwC1EhBelW12Zy26JeOUkQ5mRu+5o8rpsJk5+2t+Y5vE7e842qtZDQ2g1NpX/29HdyFeJ4nSIhI47ENSxlQ=="],
|
||||||
|
|
@ -1691,7 +1680,7 @@
|
||||||
|
|
||||||
"y18n": ["y18n@5.0.8", "", {}, "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA=="],
|
"y18n": ["y18n@5.0.8", "", {}, "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA=="],
|
||||||
|
|
||||||
"yallist": ["yallist@5.0.0", "", {}, "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw=="],
|
"yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="],
|
||||||
|
|
||||||
"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=="],
|
"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=="],
|
||||||
|
|
||||||
|
|
@ -1821,8 +1810,6 @@
|
||||||
|
|
||||||
"load-json-file/pify": ["pify@3.0.0", "", {}, "sha512-C3FsVNH1udSEX48gGX1xfvwTWfsYWj5U+8/uK15BGzIGrKoUpghX8hWZwa/OFnakBiiVNmBvemTJR5mcy7iPcg=="],
|
"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/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=="],
|
"meow/type-fest": ["type-fest@0.18.1", "", {}, "sha512-OIAYXk8+ISY+qTOwkHtKqzAuxchoMiD9Udx+FSGQDuiRR+PJKJHc2NJAXlbhkGwTt/4/nKZxELY1w3ReWOL8mw=="],
|
||||||
|
|
|
||||||
|
|
@ -39,7 +39,8 @@
|
||||||
"build:dev:appimage": "bun run build && bun run ./scripts/build-appimage.ts",
|
"build:dev:appimage": "bun run build && bun run ./scripts/build-appimage.ts",
|
||||||
"version:generate": "standard-version --sign",
|
"version:generate": "standard-version --sign",
|
||||||
"package:Linux": "bun run build:prod:appimage",
|
"package:Linux": "bun run build:prod:appimage",
|
||||||
"package:Windows": "bun run build:prod"
|
"package:Windows": "bun run build:prod",
|
||||||
|
"download:chromium": "bun scripts/download-chromium.ts --out=./bin/chromium"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"7zip-bin": "^5.2.0",
|
"7zip-bin": "^5.2.0",
|
||||||
|
|
@ -47,7 +48,6 @@
|
||||||
"@elysiajs/cors": "^1.4.1",
|
"@elysiajs/cors": "^1.4.1",
|
||||||
"@elysiajs/eden": "^1.4.6",
|
"@elysiajs/eden": "^1.4.6",
|
||||||
"@jimp/wasm-webp": "^1.6.0",
|
"@jimp/wasm-webp": "^1.6.0",
|
||||||
"@kmamal/sdl": "^0.11.13",
|
|
||||||
"cheerio": "^1.2.0",
|
"cheerio": "^1.2.0",
|
||||||
"conf": "^15.0.2",
|
"conf": "^15.0.2",
|
||||||
"drizzle-orm": "^0.45.1",
|
"drizzle-orm": "^0.45.1",
|
||||||
|
|
|
||||||
283
scripts/download-chromium.ts
Normal file
283
scripts/download-chromium.ts
Normal file
|
|
@ -0,0 +1,283 @@
|
||||||
|
#!/usr/bin/env bun
|
||||||
|
/**
|
||||||
|
* download-chromium.ts
|
||||||
|
* Downloads the latest ungoogled-chromium for the current platform + arch.
|
||||||
|
* Skips the download if the binary is already present and up to date.
|
||||||
|
*
|
||||||
|
* Usage: bun download-chromium.ts [--out=./chromium] [--force]
|
||||||
|
* In package.json scripts: "prebuild": "bun scripts/download-chromium.ts"
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { mkdir } from "node:fs/promises";
|
||||||
|
import { existsSync } from "node:fs";
|
||||||
|
import path from "node:path";
|
||||||
|
import { spawnSync } from "node:child_process";
|
||||||
|
import StreamZip from "node-stream-zip";
|
||||||
|
|
||||||
|
// --- Config ------------------------------------------------------------------
|
||||||
|
|
||||||
|
const GITHUB_API = "https://api.github.com";
|
||||||
|
const VERSION_FILE = ".chromium-version";
|
||||||
|
|
||||||
|
const REPOS: Record<string, string> = {
|
||||||
|
linux: "ungoogled-software/ungoogled-chromium-portablelinux",
|
||||||
|
darwin: "ungoogled-software/ungoogled-chromium-macos",
|
||||||
|
win32: "ungoogled-software/ungoogled-chromium-windows",
|
||||||
|
};
|
||||||
|
|
||||||
|
const PLATFORM_MAP: Record<string, string> = {
|
||||||
|
linux: "linux",
|
||||||
|
win32: "windows",
|
||||||
|
darwin: 'macos'
|
||||||
|
};
|
||||||
|
|
||||||
|
const ARCH_MAP: Record<string, Record<string, string>> = {
|
||||||
|
linux: { x64: "x86_64", arm64: "arm64" },
|
||||||
|
darwin: { x64: "x86_64", arm64: "arm64" },
|
||||||
|
win32: { x64: "x64", arm64: "arm64" },
|
||||||
|
};
|
||||||
|
|
||||||
|
const PREFERRED_EXT: Record<string, string[]> = {
|
||||||
|
linux: [".tar.xz"],
|
||||||
|
darwin: [".dmg", ".zip"],
|
||||||
|
win32: [".zip"],
|
||||||
|
};
|
||||||
|
|
||||||
|
/** The expected binary path per platform after extraction */
|
||||||
|
function getBinaryPath (outDir: string, version: string, platform: string, arch: string): string
|
||||||
|
{
|
||||||
|
const subFolder = `ungoogled-chromium_${version}_${PLATFORM_MAP[platform]}_${ARCH_MAP[platform][arch]}`;
|
||||||
|
if (platform === "linux")
|
||||||
|
{
|
||||||
|
return path.join(outDir, subFolder, "chrome");
|
||||||
|
}
|
||||||
|
if (platform === "darwin") return path.join(outDir, "Chromium.app");
|
||||||
|
return path.join(outDir, subFolder, "chrome.exe");
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Helpers -----------------------------------------------------------------
|
||||||
|
|
||||||
|
function log (msg: string)
|
||||||
|
{
|
||||||
|
process.stdout.write(`\x1b[36m[chromium]\x1b[0m ${msg}\n`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function err (msg: string): never
|
||||||
|
{
|
||||||
|
process.stderr.write(`\x1b[31m[error]\x1b[0m ${msg}\n`);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function ghFetch (url: string)
|
||||||
|
{
|
||||||
|
const headers: Record<string, string> = { "User-Agent": "bun-chromium-downloader" };
|
||||||
|
const token = process.env.GITHUB_TOKEN;
|
||||||
|
if (token) headers["Authorization"] = `Bearer ${token}`;
|
||||||
|
const res = await fetch(url, { headers });
|
||||||
|
if (!res.ok) err(`GitHub API error ${res.status}: ${url}`);
|
||||||
|
return res.json();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function readVersionCache (outDir: string): Promise<string | null>
|
||||||
|
{
|
||||||
|
const file = path.join(outDir, VERSION_FILE);
|
||||||
|
if (!existsSync(file)) return null;
|
||||||
|
return (await Bun.file(file).text()).trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function writeVersionCache (outDir: string, version: string)
|
||||||
|
{
|
||||||
|
await Bun.write(path.join(outDir, VERSION_FILE), version);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function downloadWithProgress (url: string, dest: string)
|
||||||
|
{
|
||||||
|
log(`Downloading -> ${dest}`);
|
||||||
|
const res = await fetch(url);
|
||||||
|
if (!res.ok) err(`Download failed: ${res.status} ${url}`);
|
||||||
|
|
||||||
|
const total = Number(res.headers.get("content-length") ?? 0);
|
||||||
|
let received = 0;
|
||||||
|
const writer = Bun.file(dest).writer();
|
||||||
|
const reader = res.body!.getReader();
|
||||||
|
|
||||||
|
while (true)
|
||||||
|
{
|
||||||
|
const { done, value } = await reader.read();
|
||||||
|
if (done) break;
|
||||||
|
writer.write(value);
|
||||||
|
received += value.length;
|
||||||
|
if (total > 0)
|
||||||
|
{
|
||||||
|
const pct = ((received / total) * 100).toFixed(1);
|
||||||
|
const mb = (received / 1e6).toFixed(1);
|
||||||
|
const totalMb = (total / 1e6).toFixed(1);
|
||||||
|
process.stdout.write(`\r ${pct}% ${mb} / ${totalMb} MB `);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await writer.end();
|
||||||
|
process.stdout.write("\n");
|
||||||
|
log("Download complete.");
|
||||||
|
}
|
||||||
|
|
||||||
|
async function extractZip (src: string, outDir: string)
|
||||||
|
{
|
||||||
|
log(`Extracting zip -> ${outDir}`);
|
||||||
|
const zip = new StreamZip.async({ file: src });
|
||||||
|
const entries = await zip.entries();
|
||||||
|
const total = Object.keys(entries).length;
|
||||||
|
await zip.extract(null, outDir);
|
||||||
|
await zip.close();
|
||||||
|
log(`Extracted ${total} files.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function extractNative (src: string, outDir: string)
|
||||||
|
{
|
||||||
|
if (src.endsWith(".AppImage"))
|
||||||
|
{
|
||||||
|
const dest = path.join(outDir, "chromium.AppImage");
|
||||||
|
spawnSync("cp", [src, dest]);
|
||||||
|
spawnSync("chmod", ["+x", dest]);
|
||||||
|
log(`AppImage ready at: ${dest}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (src.endsWith(".tar.xz"))
|
||||||
|
{
|
||||||
|
const result = spawnSync("tar", ["-xJf", src, "-C", outDir], { stdio: "inherit" });
|
||||||
|
if (result.status !== 0) err("tar extraction failed");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (src.endsWith(".dmg"))
|
||||||
|
{
|
||||||
|
log("Mounting DMG...");
|
||||||
|
const mount = spawnSync("hdiutil", ["attach", src, "-nobrowse", "-quiet"], {
|
||||||
|
encoding: "utf8",
|
||||||
|
});
|
||||||
|
if (mount.status !== 0) err("hdiutil mount failed");
|
||||||
|
const mountLine = mount.stdout.split("\n").find((l) => l.includes("/Volumes/"));
|
||||||
|
const mountPoint = mountLine?.split("\t").at(-1)?.trim();
|
||||||
|
if (!mountPoint) err("Could not find DMG mount point");
|
||||||
|
spawnSync("cp", ["-R", mountPoint!, outDir], { stdio: "inherit" });
|
||||||
|
spawnSync("hdiutil", ["detach", mountPoint!, "-quiet"]);
|
||||||
|
log(`DMG contents copied to: ${outDir}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
err(`Unknown archive format: ${src}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Main --------------------------------------------------------------------
|
||||||
|
|
||||||
|
async function main ()
|
||||||
|
{
|
||||||
|
const platform = process.platform;
|
||||||
|
const arch = process.arch;
|
||||||
|
const force = process.argv.includes("--force");
|
||||||
|
const outArg = process.argv.find(a => a.startsWith("--out="))?.slice(6)
|
||||||
|
?? "./chromium";
|
||||||
|
const outDir = path.resolve(outArg);
|
||||||
|
|
||||||
|
log(`Platform: ${platform} Arch: ${arch}`);
|
||||||
|
|
||||||
|
const repo = REPOS[platform];
|
||||||
|
if (!repo) err(`Unsupported platform: ${platform}`);
|
||||||
|
|
||||||
|
const archStr = ARCH_MAP[platform]?.[arch];
|
||||||
|
if (!archStr) err(`Unsupported arch "${arch}" on ${platform}`);
|
||||||
|
|
||||||
|
// Fetch latest version (lightweight — just the tag, no asset download yet)
|
||||||
|
log(`Checking latest release from ${repo}...`);
|
||||||
|
const release = await ghFetch(`${GITHUB_API}/repos/${repo}/releases/latest`);
|
||||||
|
const version: string = release.tag_name ?? release.name ?? "unknown";
|
||||||
|
log(`Latest version: ${version}`);
|
||||||
|
|
||||||
|
// Check if already downloaded and up to date
|
||||||
|
if (!force)
|
||||||
|
{
|
||||||
|
const cachedVersion = await readVersionCache(outDir);
|
||||||
|
const assets: Array<{ name: string; }> = release.assets ?? [];
|
||||||
|
const preferred = PREFERRED_EXT[platform] ?? [];
|
||||||
|
let assetName: string | undefined;
|
||||||
|
for (const ext of preferred)
|
||||||
|
{
|
||||||
|
assetName = assets.find(a => a.name.includes(archStr) && a.name.endsWith(ext))?.name;
|
||||||
|
if (assetName) break;
|
||||||
|
}
|
||||||
|
if (!assetName) assetName = assets.find(a => a.name.includes(archStr))?.name;
|
||||||
|
|
||||||
|
if (cachedVersion === version)
|
||||||
|
{
|
||||||
|
const binaryPath = getBinaryPath(outDir, cachedVersion, platform, arch);
|
||||||
|
if (existsSync(binaryPath))
|
||||||
|
{
|
||||||
|
log(`Already up to date (${version}). Skipping download.`);
|
||||||
|
log(`Binary: ${binaryPath}`);
|
||||||
|
return;
|
||||||
|
} else
|
||||||
|
{
|
||||||
|
log(`Version matches but binary missing — re-downloading.`);
|
||||||
|
}
|
||||||
|
} else if (cachedVersion)
|
||||||
|
{
|
||||||
|
log(`New version available: ${cachedVersion} -> ${version}`);
|
||||||
|
}
|
||||||
|
} else
|
||||||
|
{
|
||||||
|
log("--force flag set, re-downloading.");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pick asset to download
|
||||||
|
const assets: Array<{ name: string; browser_download_url: string; }> = release.assets ?? [];
|
||||||
|
if (assets.length === 0) err("No assets found in the latest release.");
|
||||||
|
|
||||||
|
const preferred = PREFERRED_EXT[platform] ?? [];
|
||||||
|
let chosen: (typeof assets)[0] | undefined;
|
||||||
|
|
||||||
|
for (const ext of preferred)
|
||||||
|
{
|
||||||
|
chosen = assets.find(a => a.name.includes(archStr) && a.name.endsWith(ext));
|
||||||
|
if (chosen) break;
|
||||||
|
}
|
||||||
|
if (!chosen) chosen = assets.find(a => a.name.includes(archStr));
|
||||||
|
|
||||||
|
if (!chosen)
|
||||||
|
{
|
||||||
|
log("Available assets:");
|
||||||
|
for (const a of assets) log(` ${a.name}`);
|
||||||
|
err(`No asset found matching arch "${archStr}" on ${platform}.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
log(`Selected asset: ${chosen.name}`);
|
||||||
|
|
||||||
|
if (!existsSync(outDir)) await mkdir(outDir, { recursive: true });
|
||||||
|
|
||||||
|
const tmpFile = path.join(outDir, chosen.name);
|
||||||
|
await downloadWithProgress(chosen.browser_download_url, tmpFile);
|
||||||
|
|
||||||
|
const { unlink } = await import("node:fs/promises");
|
||||||
|
|
||||||
|
if (chosen.name.endsWith(".zip"))
|
||||||
|
{
|
||||||
|
await extractZip(tmpFile, outDir);
|
||||||
|
await unlink(tmpFile);
|
||||||
|
} else
|
||||||
|
{
|
||||||
|
extractNative(tmpFile, outDir);
|
||||||
|
if (!chosen.name.endsWith(".AppImage"))
|
||||||
|
{
|
||||||
|
await unlink(tmpFile);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save version so next run can skip
|
||||||
|
await writeVersionCache(outDir, version);
|
||||||
|
|
||||||
|
log(`\nDone! Chromium ${version} extracted to: ${outDir}`);
|
||||||
|
|
||||||
|
const binaryPath = getBinaryPath(outDir, version, platform, arch);
|
||||||
|
log(`Binary: ${binaryPath}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch((e) => err(String(e)));
|
||||||
|
|
@ -95,8 +95,9 @@ await Bun.build({
|
||||||
await fs.cp('./drizzle', `${buildSubDir}/drizzle`, { recursive: true });
|
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(`./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(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 });
|
await fs.cp(`node_modules/7zip-bin/${zipNodePath}/${process.arch}`, buildSubDir, { recursive: true, errorOnExist: false });
|
||||||
|
if (await fs.exists('bin/chromium'))
|
||||||
|
await fs.cp('bin/chromium', `${buildSubDir}/bin/chromium`, { recursive: true, errorOnExist: false });
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
}]
|
}]
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,7 @@ import UpdateStoreJob from "./jobs/update-store";
|
||||||
import { getStoreFolder } from "./store/services/gamesService";
|
import { getStoreFolder } from "./store/services/gamesService";
|
||||||
import { PluginManager } from "./plugins/plugin-manager";
|
import { PluginManager } from "./plugins/plugin-manager";
|
||||||
import registerPlugins from "./plugins/register-plugins";
|
import registerPlugins from "./plugins/register-plugins";
|
||||||
import controls from '../controls';
|
import controls from './controls/controls';
|
||||||
|
|
||||||
export const config = new Conf<SettingsType>({
|
export const config = new Conf<SettingsType>({
|
||||||
projectName: projectPackage.name,
|
projectName: projectPackage.name,
|
||||||
|
|
|
||||||
40
src/bun/api/controls/controls.ts
Normal file
40
src/bun/api/controls/controls.ts
Normal file
|
|
@ -0,0 +1,40 @@
|
||||||
|
import { LaunchGameJob } from '../jobs/launch-game-job';
|
||||||
|
import { events, taskQueue } from '../app';
|
||||||
|
import { GamepadManager } from './manager';
|
||||||
|
|
||||||
|
export default async function Initialize ()
|
||||||
|
{
|
||||||
|
let startSelectPressed = false;
|
||||||
|
|
||||||
|
const manager = new GamepadManager();
|
||||||
|
|
||||||
|
setInterval(() =>
|
||||||
|
{
|
||||||
|
for (const pad of manager.getGamepads())
|
||||||
|
{
|
||||||
|
const state = pad.update();
|
||||||
|
if (!state) continue;
|
||||||
|
|
||||||
|
if (state.buttons.START && state.buttons.SELECT)
|
||||||
|
{
|
||||||
|
if (!startSelectPressed)
|
||||||
|
{
|
||||||
|
startSelectPressed = true;
|
||||||
|
console.log("Focus");
|
||||||
|
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');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else
|
||||||
|
{
|
||||||
|
startSelectPressed = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, 100);
|
||||||
|
}
|
||||||
32
src/bun/api/controls/gamepad.ts
Normal file
32
src/bun/api/controls/gamepad.ts
Normal file
|
|
@ -0,0 +1,32 @@
|
||||||
|
// ./gamepad/index.ts
|
||||||
|
|
||||||
|
import { platform } from "os";
|
||||||
|
import { GamepadWindows } from "./windows";
|
||||||
|
import { GamepadLinux } from "./linux";
|
||||||
|
import type { IGamepadBackend, GamepadState } from "./types";
|
||||||
|
|
||||||
|
export class Gamepad
|
||||||
|
{
|
||||||
|
private backend: IGamepadBackend;
|
||||||
|
|
||||||
|
constructor(index = 0)
|
||||||
|
{
|
||||||
|
if (platform() === "win32")
|
||||||
|
{
|
||||||
|
this.backend = new GamepadWindows(index);
|
||||||
|
} else
|
||||||
|
{
|
||||||
|
this.backend = new GamepadLinux(index);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
update (): GamepadState | null
|
||||||
|
{
|
||||||
|
return this.backend.update();
|
||||||
|
}
|
||||||
|
|
||||||
|
close ()
|
||||||
|
{
|
||||||
|
this.backend.close?.();
|
||||||
|
}
|
||||||
|
}
|
||||||
87
src/bun/api/controls/linux.ts
Normal file
87
src/bun/api/controls/linux.ts
Normal file
|
|
@ -0,0 +1,87 @@
|
||||||
|
import { IGamepadBackend, GamepadState, ButtonName } from "./types";
|
||||||
|
import { openSync, readSync, closeSync, readdirSync } from "fs";
|
||||||
|
|
||||||
|
export class GamepadLinux implements IGamepadBackend
|
||||||
|
{
|
||||||
|
private fd: number;
|
||||||
|
private buttons: boolean[];
|
||||||
|
private axes: number[];
|
||||||
|
private buttonsCount = 16;
|
||||||
|
private axesCount = 4;
|
||||||
|
|
||||||
|
constructor(index = 0)
|
||||||
|
{
|
||||||
|
const devices = readdirSync("/dev/input").filter(f => f.startsWith("js"));
|
||||||
|
if (!devices[index]) throw new Error("No gamepad found");
|
||||||
|
const path = `/dev/input/${devices[index]}`;
|
||||||
|
this.fd = openSync(path, "r");
|
||||||
|
|
||||||
|
this.buttons = Array(this.buttonsCount).fill(false);
|
||||||
|
this.axes = Array(this.axesCount).fill(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
update (): GamepadState | null
|
||||||
|
{
|
||||||
|
const buf = Buffer.alloc(8);
|
||||||
|
let bytesRead;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
bytesRead = readSync(this.fd, buf, 0, 8, null);
|
||||||
|
} catch
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (bytesRead !== 8) return null;
|
||||||
|
|
||||||
|
const [time, value, type, number] = [
|
||||||
|
buf.readUInt32LE(0),
|
||||||
|
buf.readInt16LE(4),
|
||||||
|
buf[6],
|
||||||
|
buf[7],
|
||||||
|
];
|
||||||
|
|
||||||
|
if (type === 1) this.buttons[number] = value !== 0;
|
||||||
|
else if (type === 2 && number < 4) this.axes[number] = value / 32767;
|
||||||
|
|
||||||
|
const btnMap: Record<ButtonName, boolean> = {
|
||||||
|
A: this.buttons[0] ?? false,
|
||||||
|
B: this.buttons[1] ?? false,
|
||||||
|
X: this.buttons[2] ?? false,
|
||||||
|
Y: this.buttons[3] ?? false,
|
||||||
|
UP: this.buttons[4] ?? false,
|
||||||
|
DOWN: this.buttons[5] ?? false,
|
||||||
|
LEFT: this.buttons[6] ?? false,
|
||||||
|
RIGHT: this.buttons[7] ?? false,
|
||||||
|
LB: this.buttons[8] ?? false,
|
||||||
|
RB: this.buttons[9] ?? false,
|
||||||
|
START: this.buttons[10] ?? false,
|
||||||
|
SELECT: this.buttons[11] ?? false,
|
||||||
|
L3: this.buttons[12] ?? false,
|
||||||
|
R3: this.buttons[13] ?? false,
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
buttons: btnMap,
|
||||||
|
leftStick: { x: this.axes[0] ?? 0, y: this.axes[1] ?? 0 },
|
||||||
|
rightStick: { x: this.axes[2] ?? 0, y: this.axes[3] ?? 0 },
|
||||||
|
triggers: { left: 0, right: 0 },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
isConnected ()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
readSync(this.fd, Buffer.alloc(1), 0, 1, null);
|
||||||
|
return true;
|
||||||
|
} catch
|
||||||
|
{
|
||||||
|
return false; // file disappeared or read failed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
close ()
|
||||||
|
{
|
||||||
|
closeSync(this.fd);
|
||||||
|
}
|
||||||
|
}
|
||||||
55
src/bun/api/controls/manager.ts
Normal file
55
src/bun/api/controls/manager.ts
Normal file
|
|
@ -0,0 +1,55 @@
|
||||||
|
import { Gamepad } from "./gamepad";
|
||||||
|
import { platform } from "os";
|
||||||
|
|
||||||
|
export class GamepadManager
|
||||||
|
{
|
||||||
|
private gamepads: Gamepad[] = [];
|
||||||
|
private scanInterval: any;
|
||||||
|
|
||||||
|
constructor()
|
||||||
|
{
|
||||||
|
this.scanGamepads();
|
||||||
|
// scan every second for new/disconnected devices
|
||||||
|
this.scanInterval = setInterval(() => this.scanGamepads(), 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
private scanGamepads ()
|
||||||
|
{
|
||||||
|
const max = platform() === "win32" ? 4 : 8; // max controllers
|
||||||
|
for (let i = 0; i < max; i++)
|
||||||
|
{
|
||||||
|
if (!this.gamepads[i])
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
const pad = new Gamepad(i);
|
||||||
|
if (pad.update())
|
||||||
|
{
|
||||||
|
this.gamepads[i] = pad;
|
||||||
|
console.log(`Gamepad ${i} connected`);
|
||||||
|
}
|
||||||
|
} catch { }
|
||||||
|
} else
|
||||||
|
{
|
||||||
|
const connected = this.gamepads[i].update() !== null;
|
||||||
|
if (!connected)
|
||||||
|
{
|
||||||
|
console.log(`Gamepad ${i} disconnected`);
|
||||||
|
this.gamepads[i].close();
|
||||||
|
delete this.gamepads[i];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getGamepads ()
|
||||||
|
{
|
||||||
|
return this.gamepads.filter(Boolean);
|
||||||
|
}
|
||||||
|
|
||||||
|
stop ()
|
||||||
|
{
|
||||||
|
clearInterval(this.scanInterval);
|
||||||
|
for (const pad of this.gamepads) pad.close?.();
|
||||||
|
}
|
||||||
|
}
|
||||||
38
src/bun/api/controls/types.ts
Normal file
38
src/bun/api/controls/types.ts
Normal file
|
|
@ -0,0 +1,38 @@
|
||||||
|
export type ButtonName =
|
||||||
|
| "A" | "B" | "X" | "Y"
|
||||||
|
| "UP" | "DOWN" | "LEFT" | "RIGHT"
|
||||||
|
| "LB" | "RB"
|
||||||
|
| "START" | "SELECT"
|
||||||
|
| "L3" | "R3";
|
||||||
|
|
||||||
|
export interface Stick
|
||||||
|
{
|
||||||
|
x: number; // -1 → 1
|
||||||
|
y: number; // -1 → 1
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Triggers
|
||||||
|
{
|
||||||
|
left: number; // 0 → 1
|
||||||
|
right: number; // 0 → 1
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GamepadState
|
||||||
|
{
|
||||||
|
buttons: Record<ButtonName, boolean>;
|
||||||
|
leftStick: Stick;
|
||||||
|
rightStick: Stick;
|
||||||
|
triggers: Triggers;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IGamepadBackend
|
||||||
|
{
|
||||||
|
/** Polls the current state; returns null if disconnected */
|
||||||
|
update (): GamepadState | null;
|
||||||
|
|
||||||
|
/** Optional: release resources (like closing fd on Linux) */
|
||||||
|
close?(): void;
|
||||||
|
|
||||||
|
/** Optional: check if the gamepad is still connected */
|
||||||
|
isConnected?(): boolean;
|
||||||
|
}
|
||||||
57
src/bun/api/controls/windows.ts
Normal file
57
src/bun/api/controls/windows.ts
Normal file
|
|
@ -0,0 +1,57 @@
|
||||||
|
import { IGamepadBackend, GamepadState, ButtonName } from "./types";
|
||||||
|
import { dlopen, FFIType } from "bun:ffi";
|
||||||
|
|
||||||
|
const xinput = dlopen("xinput1_4.dll", {
|
||||||
|
XInputGetState: { args: [FFIType.u32, FFIType.ptr], returns: FFIType.u32 },
|
||||||
|
});
|
||||||
|
const ERROR_SUCCESS = 0;
|
||||||
|
|
||||||
|
export class GamepadWindows implements IGamepadBackend
|
||||||
|
{
|
||||||
|
private index: number;
|
||||||
|
private buffer = new ArrayBuffer(16);
|
||||||
|
private view = new DataView(this.buffer);
|
||||||
|
private prevButtons = 0;
|
||||||
|
private currButtons = 0;
|
||||||
|
|
||||||
|
constructor(index = 0) { this.index = index; }
|
||||||
|
|
||||||
|
update (): GamepadState | null
|
||||||
|
{
|
||||||
|
const res = xinput.symbols.XInputGetState(this.index, this.buffer);
|
||||||
|
if (res !== ERROR_SUCCESS) return null;
|
||||||
|
|
||||||
|
this.prevButtons = this.currButtons;
|
||||||
|
this.currButtons = this.view.getUint16(4, true);
|
||||||
|
|
||||||
|
const btns: Record<ButtonName, boolean> = {
|
||||||
|
A: (this.currButtons & 0x1000) !== 0,
|
||||||
|
B: (this.currButtons & 0x2000) !== 0,
|
||||||
|
X: (this.currButtons & 0x4000) !== 0,
|
||||||
|
Y: (this.currButtons & 0x8000) !== 0,
|
||||||
|
UP: (this.currButtons & 0x0001) !== 0,
|
||||||
|
DOWN: (this.currButtons & 0x0002) !== 0,
|
||||||
|
LEFT: (this.currButtons & 0x0004) !== 0,
|
||||||
|
RIGHT: (this.currButtons & 0x0008) !== 0,
|
||||||
|
LB: (this.currButtons & 0x0100) !== 0,
|
||||||
|
RB: (this.currButtons & 0x0200) !== 0,
|
||||||
|
START: (this.currButtons & 0x0010) !== 0,
|
||||||
|
SELECT: (this.currButtons & 0x0020) !== 0,
|
||||||
|
L3: (this.currButtons & 0x0040) !== 0,
|
||||||
|
R3: (this.currButtons & 0x0080) !== 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
buttons: btns,
|
||||||
|
leftStick: { x: this.view.getInt16(6, true) / 32767, y: this.view.getInt16(8, true) / 32767 },
|
||||||
|
rightStick: { x: this.view.getInt16(10, true) / 32767, y: this.view.getInt16(12, true) / 32767 },
|
||||||
|
triggers: { left: this.view.getUint8(14) / 255, right: this.view.getUint8(15) / 255 },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
isConnected ()
|
||||||
|
{
|
||||||
|
const res = xinput.symbols.XInputGetState(this.index, this.buffer);
|
||||||
|
return res === ERROR_SUCCESS;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -2,7 +2,7 @@ import { killBrowser, spawnBrowser } from './utils/browser-spawner';
|
||||||
import { BrowserParams, BuildParams } from './utils/browser-params';
|
import { BrowserParams, BuildParams } from './utils/browser-params';
|
||||||
import os from 'node:os';
|
import os from 'node:os';
|
||||||
import { EventEmitter } from 'node:stream';
|
import { EventEmitter } from 'node:stream';
|
||||||
import { dlopen, FFIType } from "bun:ffi";
|
import { dlopen, FFIType, Pointer } from "bun:ffi";
|
||||||
|
|
||||||
export default async function init (events: EventEmitter, forceBrowser: boolean, params: BrowserParams)
|
export default async function init (events: EventEmitter, forceBrowser: boolean, params: BrowserParams)
|
||||||
{
|
{
|
||||||
|
|
@ -21,6 +21,29 @@ export default async function init (events: EventEmitter, forceBrowser: boolean,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function focusWindow (id: Pointer)
|
||||||
|
{
|
||||||
|
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 (id)
|
||||||
|
{
|
||||||
|
user32.symbols.ShowWindow(id, SW_RESTORE);
|
||||||
|
user32.symbols.keybd_event(0, 0, 0, null); // fake input event
|
||||||
|
user32.symbols.BringWindowToTop(id);
|
||||||
|
user32.symbols.SetForegroundWindow(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function runWebview (events: EventEmitter, params: BrowserParams)
|
async function runWebview (events: EventEmitter, params: BrowserParams)
|
||||||
{
|
{
|
||||||
const webviewPath = process.env.IS_BINARY ? `./webview/${os.platform()}` : new URL(`./webview/${os.platform()}`, import.meta.url).href;
|
const webviewPath = process.env.IS_BINARY ? `./webview/${os.platform()}` : new URL(`./webview/${os.platform()}`, import.meta.url).href;
|
||||||
|
|
@ -73,25 +96,7 @@ async function runWebview (events: EventEmitter, params: BrowserParams)
|
||||||
events.on('exitapp', handleExit);
|
events.on('exitapp', handleExit);
|
||||||
events.on('focus', () =>
|
events.on('focus', () =>
|
||||||
{
|
{
|
||||||
if (process.platform === 'win32')
|
focusWindow(pointer);
|
||||||
{
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,53 +0,0 @@
|
||||||
|
|
||||||
|
|
||||||
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;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -50,7 +50,7 @@ if (process.env.HEADLESS)
|
||||||
});
|
});
|
||||||
} else
|
} else
|
||||||
{
|
{
|
||||||
await init(app.events, Bun.env.FORCE_BROWSER === "true", {
|
await init(app.events, process.env.FORCE_BROWSER === "true", {
|
||||||
configPath: dirname(app.config.path),
|
configPath: dirname(app.config.path),
|
||||||
windowPosition: app.config.get('windowPosition'),
|
windowPosition: app.config.get('windowPosition'),
|
||||||
windowSize: app.config.get('windowSize'),
|
windowSize: app.config.get('windowSize'),
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ import { ChildProcessWithoutNullStreams } from "node:child_process";
|
||||||
import os from 'node:os';
|
import os from 'node:os';
|
||||||
|
|
||||||
export type RunBrowserType = "chrome" | "chromium" | "firefox" | "edge";
|
export type RunBrowserType = "chrome" | "chromium" | "firefox" | "edge";
|
||||||
export type RunBrowserSource = "running" | "system" | "flatpak";
|
export type RunBrowserSource = "running" | "system" | "flatpak" | "bundled";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Options for spawning a browser process.
|
* Options for spawning a browser process.
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,10 @@
|
||||||
import { spawnSync } from "bun";
|
import { spawnSync } from "bun";
|
||||||
import { platform } from "node:os";
|
import { platform } from "node:os";
|
||||||
import { RunBrowserType } from "./browser-spawner";
|
import { RunBrowserType } from "./browser-spawner";
|
||||||
|
import path from 'node:path';
|
||||||
|
|
||||||
export type GetBrowserType = "chrome" | "chromium" | "firefox";
|
export type GetBrowserType = "chrome" | "chromium" | "firefox";
|
||||||
export type GetBrowserSource = "running" | "system" | "flatpak";
|
export type GetBrowserSource = "running" | "system" | "flatpak" | "bundled";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Browser discovery priority configuration
|
* Browser discovery priority configuration
|
||||||
|
|
@ -12,6 +13,7 @@ interface BrowserPriorityConfig
|
||||||
{
|
{
|
||||||
/** Include currently running browser processes in search */
|
/** Include currently running browser processes in search */
|
||||||
includeRunning?: boolean;
|
includeRunning?: boolean;
|
||||||
|
includeBundled?: boolean;
|
||||||
/** Browser types to search for, in priority order */
|
/** Browser types to search for, in priority order */
|
||||||
browserOrder?: GetBrowserType[];
|
browserOrder?: GetBrowserType[];
|
||||||
/** Include system default browser on Windows */
|
/** Include system default browser on Windows */
|
||||||
|
|
@ -33,6 +35,27 @@ interface BrowserResult
|
||||||
source: GetBrowserSource;
|
source: GetBrowserSource;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const PLATFORM_MAP: Record<string, string> = {
|
||||||
|
linux: "linux",
|
||||||
|
win32: "windows",
|
||||||
|
darwin: 'macos'
|
||||||
|
};
|
||||||
|
|
||||||
|
const ARCH_MAP: Record<string, Record<string, string>> = {
|
||||||
|
linux: { x64: "x86_64", arm64: "arm64" },
|
||||||
|
darwin: { x64: "x86_64", arm64: "arm64" },
|
||||||
|
win32: { x64: "x64", arm64: "arm64" },
|
||||||
|
};
|
||||||
|
|
||||||
|
/** The expected binary path per platform after extraction */
|
||||||
|
function getBundledBinaryPath (outDir: string, version: string, platform: string, arch: string): string
|
||||||
|
{
|
||||||
|
const subFolder = `ungoogled-chromium_${version}_${PLATFORM_MAP[platform]}_${ARCH_MAP[platform][arch]}`;
|
||||||
|
if (platform === "linux") return path.join(outDir, subFolder, "chrome");
|
||||||
|
if (platform === "darwin") return path.join(outDir, "Chromium.app");
|
||||||
|
return path.join(outDir, subFolder, "chrome.exe");
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Main function to find a valid browser executable.
|
* Main function to find a valid browser executable.
|
||||||
*
|
*
|
||||||
|
|
@ -63,6 +86,7 @@ export async function getBrowserPath (config?: BrowserPriorityConfig): Promise<B
|
||||||
// Default configuration
|
// Default configuration
|
||||||
const {
|
const {
|
||||||
includeRunning = true,
|
includeRunning = true,
|
||||||
|
includeBundled = true,
|
||||||
browserOrder = ["firefox", "chrome", "chromium"],
|
browserOrder = ["firefox", "chrome", "chromium"],
|
||||||
includeSystemDefault = true,
|
includeSystemDefault = true,
|
||||||
includeFlatpak = true
|
includeFlatpak = true
|
||||||
|
|
@ -70,6 +94,17 @@ export async function getBrowserPath (config?: BrowserPriorityConfig): Promise<B
|
||||||
|
|
||||||
const currentPlatform = platform();
|
const currentPlatform = platform();
|
||||||
|
|
||||||
|
// Check bundled
|
||||||
|
if (includeBundled)
|
||||||
|
{
|
||||||
|
const getVerstion = await Bun.file('./bin/chromium/.chromium-version').text();
|
||||||
|
const binPath = getBundledBinaryPath("./bin/chromium", getVerstion, process.platform, process.arch);
|
||||||
|
if (await Bun.file(binPath).exists())
|
||||||
|
{
|
||||||
|
return { path: binPath, type: "chromium", source: "bundled" };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 1. Check for currently running browser process
|
// 1. Check for currently running browser process
|
||||||
if (includeRunning)
|
if (includeRunning)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue