├── .github └── workflows │ ├── binary.yml │ └── rust.yml ├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── Makefile ├── README.md ├── assets ├── .gitignore ├── compatibilitytool.vdf ├── lib │ ├── .gitignore │ ├── greenworks.js │ ├── libsdkencryptedappticket.so │ ├── libsteam_api.so │ └── steamworksjs.js ├── package.json ├── register-hook.js └── toolmanifest.vdf ├── justfile ├── scripts └── build_steamworksjs.sh └── src ├── main.rs └── path_search.rs /.github/workflows/binary.yml: -------------------------------------------------------------------------------- 1 | name: Build binaries 2 | on: 3 | push: 4 | 5 | jobs: 6 | build: 7 | container: 8 | image: fedora:latest 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v2 12 | - name: Install dependencies 13 | run: dnf install -y gcc gcc-c++ make pkg-config musl-devel musl-libc-static musl-clang git 14 | - uses: actions-rs/toolchain@v1 15 | with: 16 | toolchain: stable 17 | target: x86_64-unknown-linux-musl 18 | 19 | - uses: actions/setup-node@v4 20 | with: 21 | node-version: 20 22 | 23 | - name: Build release binaries 24 | run: | 25 | rustup target add x86_64-unknown-linux-musl 26 | make package CARGO_ARGS="--target=x86_64-unknown-linux-musl" CARGO_TARGET="x86_64-unknown-linux-musl/release" 27 | 28 | - name: Upload artifact 29 | uses: actions/upload-artifact@v4 30 | with: 31 | name: boson-x86_64-unknown-linux-musl 32 | path: boson.tar.zst 33 | 34 | - name: Release 35 | uses: softprops/action-gh-release@v2 36 | if: startsWith(github.ref, 'refs/tags/') 37 | with: 38 | files: boson.tar.zst 39 | generate_release_notes: true 40 | make_latest: true 41 | draft: true -------------------------------------------------------------------------------- /.github/workflows/rust.yml: -------------------------------------------------------------------------------- 1 | name: Rust 2 | 3 | on: 4 | push: 5 | pull_request: 6 | 7 | jobs: 8 | rust: 9 | uses: FyraLabs/actions/.github/workflows/rust.yml@main -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /build 3 | 4 | /*.tar.* 5 | /.env* 6 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "addr2line" 7 | version = "0.21.0" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "8a30b2e23b9e17a9f90641c7ab1549cd9b44f296d3ccbf309d2863cfe398a0cb" 10 | dependencies = [ 11 | "gimli", 12 | ] 13 | 14 | [[package]] 15 | name = "adler" 16 | version = "1.0.2" 17 | source = "registry+https://github.com/rust-lang/crates.io-index" 18 | checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" 19 | 20 | [[package]] 21 | name = "aho-corasick" 22 | version = "1.1.2" 23 | source = "registry+https://github.com/rust-lang/crates.io-index" 24 | checksum = "b2969dcb958b36655471fc61f7e416fa76033bdd4bfed0678d8fee1e2d07a1f0" 25 | dependencies = [ 26 | "memchr", 27 | ] 28 | 29 | [[package]] 30 | name = "anstream" 31 | version = "0.6.11" 32 | source = "registry+https://github.com/rust-lang/crates.io-index" 33 | checksum = "6e2e1ebcb11de5c03c67de28a7df593d32191b44939c482e97702baaaa6ab6a5" 34 | dependencies = [ 35 | "anstyle", 36 | "anstyle-parse", 37 | "anstyle-query", 38 | "anstyle-wincon", 39 | "colorchoice", 40 | "utf8parse", 41 | ] 42 | 43 | [[package]] 44 | name = "anstyle" 45 | version = "1.0.6" 46 | source = "registry+https://github.com/rust-lang/crates.io-index" 47 | checksum = "8901269c6307e8d93993578286ac0edf7f195079ffff5ebdeea6a59ffb7e36bc" 48 | 49 | [[package]] 50 | name = "anstyle-parse" 51 | version = "0.2.3" 52 | source = "registry+https://github.com/rust-lang/crates.io-index" 53 | checksum = "c75ac65da39e5fe5ab759307499ddad880d724eed2f6ce5b5e8a26f4f387928c" 54 | dependencies = [ 55 | "utf8parse", 56 | ] 57 | 58 | [[package]] 59 | name = "anstyle-query" 60 | version = "1.0.2" 61 | source = "registry+https://github.com/rust-lang/crates.io-index" 62 | checksum = "e28923312444cdd728e4738b3f9c9cac739500909bb3d3c94b43551b16517648" 63 | dependencies = [ 64 | "windows-sys", 65 | ] 66 | 67 | [[package]] 68 | name = "anstyle-wincon" 69 | version = "3.0.2" 70 | source = "registry+https://github.com/rust-lang/crates.io-index" 71 | checksum = "1cd54b81ec8d6180e24654d0b371ad22fc3dd083b6ff8ba325b72e00c87660a7" 72 | dependencies = [ 73 | "anstyle", 74 | "windows-sys", 75 | ] 76 | 77 | [[package]] 78 | name = "backtrace" 79 | version = "0.3.69" 80 | source = "registry+https://github.com/rust-lang/crates.io-index" 81 | checksum = "2089b7e3f35b9dd2d0ed921ead4f6d318c27680d4a5bd167b3ee120edb105837" 82 | dependencies = [ 83 | "addr2line", 84 | "cc", 85 | "cfg-if", 86 | "libc", 87 | "miniz_oxide", 88 | "object", 89 | "rustc-demangle", 90 | ] 91 | 92 | [[package]] 93 | name = "boson" 94 | version = "0.3.0" 95 | dependencies = [ 96 | "clap", 97 | "color-eyre", 98 | "serde_json", 99 | "tracing", 100 | "tracing-subscriber", 101 | ] 102 | 103 | [[package]] 104 | name = "cc" 105 | version = "1.0.83" 106 | source = "registry+https://github.com/rust-lang/crates.io-index" 107 | checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0" 108 | dependencies = [ 109 | "libc", 110 | ] 111 | 112 | [[package]] 113 | name = "cfg-if" 114 | version = "1.0.0" 115 | source = "registry+https://github.com/rust-lang/crates.io-index" 116 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 117 | 118 | [[package]] 119 | name = "clap" 120 | version = "4.5.0" 121 | source = "registry+https://github.com/rust-lang/crates.io-index" 122 | checksum = "80c21025abd42669a92efc996ef13cfb2c5c627858421ea58d5c3b331a6c134f" 123 | dependencies = [ 124 | "clap_builder", 125 | "clap_derive", 126 | ] 127 | 128 | [[package]] 129 | name = "clap_builder" 130 | version = "4.5.0" 131 | source = "registry+https://github.com/rust-lang/crates.io-index" 132 | checksum = "458bf1f341769dfcf849846f65dffdf9146daa56bcd2a47cb4e1de9915567c99" 133 | dependencies = [ 134 | "anstream", 135 | "anstyle", 136 | "clap_lex", 137 | "strsim", 138 | ] 139 | 140 | [[package]] 141 | name = "clap_derive" 142 | version = "4.5.0" 143 | source = "registry+https://github.com/rust-lang/crates.io-index" 144 | checksum = "307bc0538d5f0f83b8248db3087aa92fe504e4691294d0c96c0eabc33f47ba47" 145 | dependencies = [ 146 | "heck", 147 | "proc-macro2", 148 | "quote", 149 | "syn", 150 | ] 151 | 152 | [[package]] 153 | name = "clap_lex" 154 | version = "0.7.0" 155 | source = "registry+https://github.com/rust-lang/crates.io-index" 156 | checksum = "98cc8fbded0c607b7ba9dd60cd98df59af97e84d24e49c8557331cfc26d301ce" 157 | 158 | [[package]] 159 | name = "color-eyre" 160 | version = "0.6.2" 161 | source = "registry+https://github.com/rust-lang/crates.io-index" 162 | checksum = "5a667583cca8c4f8436db8de46ea8233c42a7d9ae424a82d338f2e4675229204" 163 | dependencies = [ 164 | "backtrace", 165 | "color-spantrace", 166 | "eyre", 167 | "indenter", 168 | "once_cell", 169 | "owo-colors", 170 | "tracing-error", 171 | ] 172 | 173 | [[package]] 174 | name = "color-spantrace" 175 | version = "0.2.1" 176 | source = "registry+https://github.com/rust-lang/crates.io-index" 177 | checksum = "cd6be1b2a7e382e2b98b43b2adcca6bb0e465af0bdd38123873ae61eb17a72c2" 178 | dependencies = [ 179 | "once_cell", 180 | "owo-colors", 181 | "tracing-core", 182 | "tracing-error", 183 | ] 184 | 185 | [[package]] 186 | name = "colorchoice" 187 | version = "1.0.0" 188 | source = "registry+https://github.com/rust-lang/crates.io-index" 189 | checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" 190 | 191 | [[package]] 192 | name = "eyre" 193 | version = "0.6.12" 194 | source = "registry+https://github.com/rust-lang/crates.io-index" 195 | checksum = "7cd915d99f24784cdc19fd37ef22b97e3ff0ae756c7e492e9fbfe897d61e2aec" 196 | dependencies = [ 197 | "indenter", 198 | "once_cell", 199 | ] 200 | 201 | [[package]] 202 | name = "gimli" 203 | version = "0.28.1" 204 | source = "registry+https://github.com/rust-lang/crates.io-index" 205 | checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253" 206 | 207 | [[package]] 208 | name = "heck" 209 | version = "0.4.1" 210 | source = "registry+https://github.com/rust-lang/crates.io-index" 211 | checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" 212 | 213 | [[package]] 214 | name = "indenter" 215 | version = "0.3.3" 216 | source = "registry+https://github.com/rust-lang/crates.io-index" 217 | checksum = "ce23b50ad8242c51a442f3ff322d56b02f08852c77e4c0b4d3fd684abc89c683" 218 | 219 | [[package]] 220 | name = "itoa" 221 | version = "1.0.11" 222 | source = "registry+https://github.com/rust-lang/crates.io-index" 223 | checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" 224 | 225 | [[package]] 226 | name = "lazy_static" 227 | version = "1.4.0" 228 | source = "registry+https://github.com/rust-lang/crates.io-index" 229 | checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" 230 | 231 | [[package]] 232 | name = "libc" 233 | version = "0.2.153" 234 | source = "registry+https://github.com/rust-lang/crates.io-index" 235 | checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd" 236 | 237 | [[package]] 238 | name = "log" 239 | version = "0.4.20" 240 | source = "registry+https://github.com/rust-lang/crates.io-index" 241 | checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" 242 | 243 | [[package]] 244 | name = "matchers" 245 | version = "0.1.0" 246 | source = "registry+https://github.com/rust-lang/crates.io-index" 247 | checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558" 248 | dependencies = [ 249 | "regex-automata 0.1.10", 250 | ] 251 | 252 | [[package]] 253 | name = "memchr" 254 | version = "2.7.1" 255 | source = "registry+https://github.com/rust-lang/crates.io-index" 256 | checksum = "523dc4f511e55ab87b694dc30d0f820d60906ef06413f93d4d7a1385599cc149" 257 | 258 | [[package]] 259 | name = "miniz_oxide" 260 | version = "0.7.2" 261 | source = "registry+https://github.com/rust-lang/crates.io-index" 262 | checksum = "9d811f3e15f28568be3407c8e7fdb6514c1cda3cb30683f15b6a1a1dc4ea14a7" 263 | dependencies = [ 264 | "adler", 265 | ] 266 | 267 | [[package]] 268 | name = "nu-ansi-term" 269 | version = "0.46.0" 270 | source = "registry+https://github.com/rust-lang/crates.io-index" 271 | checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" 272 | dependencies = [ 273 | "overload", 274 | "winapi", 275 | ] 276 | 277 | [[package]] 278 | name = "object" 279 | version = "0.32.2" 280 | source = "registry+https://github.com/rust-lang/crates.io-index" 281 | checksum = "a6a622008b6e321afc04970976f62ee297fdbaa6f95318ca343e3eebb9648441" 282 | dependencies = [ 283 | "memchr", 284 | ] 285 | 286 | [[package]] 287 | name = "once_cell" 288 | version = "1.19.0" 289 | source = "registry+https://github.com/rust-lang/crates.io-index" 290 | checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" 291 | 292 | [[package]] 293 | name = "overload" 294 | version = "0.1.1" 295 | source = "registry+https://github.com/rust-lang/crates.io-index" 296 | checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" 297 | 298 | [[package]] 299 | name = "owo-colors" 300 | version = "3.5.0" 301 | source = "registry+https://github.com/rust-lang/crates.io-index" 302 | checksum = "c1b04fb49957986fdce4d6ee7a65027d55d4b6d2265e5848bbb507b58ccfdb6f" 303 | 304 | [[package]] 305 | name = "pin-project-lite" 306 | version = "0.2.13" 307 | source = "registry+https://github.com/rust-lang/crates.io-index" 308 | checksum = "8afb450f006bf6385ca15ef45d71d2288452bc3683ce2e2cacc0d18e4be60b58" 309 | 310 | [[package]] 311 | name = "proc-macro2" 312 | version = "1.0.78" 313 | source = "registry+https://github.com/rust-lang/crates.io-index" 314 | checksum = "e2422ad645d89c99f8f3e6b88a9fdeca7fabeac836b1002371c4367c8f984aae" 315 | dependencies = [ 316 | "unicode-ident", 317 | ] 318 | 319 | [[package]] 320 | name = "quote" 321 | version = "1.0.35" 322 | source = "registry+https://github.com/rust-lang/crates.io-index" 323 | checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef" 324 | dependencies = [ 325 | "proc-macro2", 326 | ] 327 | 328 | [[package]] 329 | name = "regex" 330 | version = "1.10.3" 331 | source = "registry+https://github.com/rust-lang/crates.io-index" 332 | checksum = "b62dbe01f0b06f9d8dc7d49e05a0785f153b00b2c227856282f671e0318c9b15" 333 | dependencies = [ 334 | "aho-corasick", 335 | "memchr", 336 | "regex-automata 0.4.5", 337 | "regex-syntax 0.8.2", 338 | ] 339 | 340 | [[package]] 341 | name = "regex-automata" 342 | version = "0.1.10" 343 | source = "registry+https://github.com/rust-lang/crates.io-index" 344 | checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" 345 | dependencies = [ 346 | "regex-syntax 0.6.29", 347 | ] 348 | 349 | [[package]] 350 | name = "regex-automata" 351 | version = "0.4.5" 352 | source = "registry+https://github.com/rust-lang/crates.io-index" 353 | checksum = "5bb987efffd3c6d0d8f5f89510bb458559eab11e4f869acb20bf845e016259cd" 354 | dependencies = [ 355 | "aho-corasick", 356 | "memchr", 357 | "regex-syntax 0.8.2", 358 | ] 359 | 360 | [[package]] 361 | name = "regex-syntax" 362 | version = "0.6.29" 363 | source = "registry+https://github.com/rust-lang/crates.io-index" 364 | checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" 365 | 366 | [[package]] 367 | name = "regex-syntax" 368 | version = "0.8.2" 369 | source = "registry+https://github.com/rust-lang/crates.io-index" 370 | checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f" 371 | 372 | [[package]] 373 | name = "rustc-demangle" 374 | version = "0.1.23" 375 | source = "registry+https://github.com/rust-lang/crates.io-index" 376 | checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" 377 | 378 | [[package]] 379 | name = "ryu" 380 | version = "1.0.18" 381 | source = "registry+https://github.com/rust-lang/crates.io-index" 382 | checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" 383 | 384 | [[package]] 385 | name = "serde" 386 | version = "1.0.210" 387 | source = "registry+https://github.com/rust-lang/crates.io-index" 388 | checksum = "c8e3592472072e6e22e0a54d5904d9febf8508f65fb8552499a1abc7d1078c3a" 389 | dependencies = [ 390 | "serde_derive", 391 | ] 392 | 393 | [[package]] 394 | name = "serde_derive" 395 | version = "1.0.210" 396 | source = "registry+https://github.com/rust-lang/crates.io-index" 397 | checksum = "243902eda00fad750862fc144cea25caca5e20d615af0a81bee94ca738f1df1f" 398 | dependencies = [ 399 | "proc-macro2", 400 | "quote", 401 | "syn", 402 | ] 403 | 404 | [[package]] 405 | name = "serde_json" 406 | version = "1.0.128" 407 | source = "registry+https://github.com/rust-lang/crates.io-index" 408 | checksum = "6ff5456707a1de34e7e37f2a6fd3d3f808c318259cbd01ab6377795054b483d8" 409 | dependencies = [ 410 | "itoa", 411 | "memchr", 412 | "ryu", 413 | "serde", 414 | ] 415 | 416 | [[package]] 417 | name = "sharded-slab" 418 | version = "0.1.7" 419 | source = "registry+https://github.com/rust-lang/crates.io-index" 420 | checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" 421 | dependencies = [ 422 | "lazy_static", 423 | ] 424 | 425 | [[package]] 426 | name = "smallvec" 427 | version = "1.13.1" 428 | source = "registry+https://github.com/rust-lang/crates.io-index" 429 | checksum = "e6ecd384b10a64542d77071bd64bd7b231f4ed5940fba55e98c3de13824cf3d7" 430 | 431 | [[package]] 432 | name = "strsim" 433 | version = "0.11.0" 434 | source = "registry+https://github.com/rust-lang/crates.io-index" 435 | checksum = "5ee073c9e4cd00e28217186dbe12796d692868f432bf2e97ee73bed0c56dfa01" 436 | 437 | [[package]] 438 | name = "syn" 439 | version = "2.0.48" 440 | source = "registry+https://github.com/rust-lang/crates.io-index" 441 | checksum = "0f3531638e407dfc0814761abb7c00a5b54992b849452a0646b7f65c9f770f3f" 442 | dependencies = [ 443 | "proc-macro2", 444 | "quote", 445 | "unicode-ident", 446 | ] 447 | 448 | [[package]] 449 | name = "thread_local" 450 | version = "1.1.7" 451 | source = "registry+https://github.com/rust-lang/crates.io-index" 452 | checksum = "3fdd6f064ccff2d6567adcb3873ca630700f00b5ad3f060c25b5dcfd9a4ce152" 453 | dependencies = [ 454 | "cfg-if", 455 | "once_cell", 456 | ] 457 | 458 | [[package]] 459 | name = "tracing" 460 | version = "0.1.40" 461 | source = "registry+https://github.com/rust-lang/crates.io-index" 462 | checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" 463 | dependencies = [ 464 | "log", 465 | "pin-project-lite", 466 | "tracing-attributes", 467 | "tracing-core", 468 | ] 469 | 470 | [[package]] 471 | name = "tracing-attributes" 472 | version = "0.1.27" 473 | source = "registry+https://github.com/rust-lang/crates.io-index" 474 | checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" 475 | dependencies = [ 476 | "proc-macro2", 477 | "quote", 478 | "syn", 479 | ] 480 | 481 | [[package]] 482 | name = "tracing-core" 483 | version = "0.1.32" 484 | source = "registry+https://github.com/rust-lang/crates.io-index" 485 | checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" 486 | dependencies = [ 487 | "once_cell", 488 | "valuable", 489 | ] 490 | 491 | [[package]] 492 | name = "tracing-error" 493 | version = "0.2.0" 494 | source = "registry+https://github.com/rust-lang/crates.io-index" 495 | checksum = "d686ec1c0f384b1277f097b2f279a2ecc11afe8c133c1aabf036a27cb4cd206e" 496 | dependencies = [ 497 | "tracing", 498 | "tracing-subscriber", 499 | ] 500 | 501 | [[package]] 502 | name = "tracing-log" 503 | version = "0.2.0" 504 | source = "registry+https://github.com/rust-lang/crates.io-index" 505 | checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" 506 | dependencies = [ 507 | "log", 508 | "once_cell", 509 | "tracing-core", 510 | ] 511 | 512 | [[package]] 513 | name = "tracing-subscriber" 514 | version = "0.3.18" 515 | source = "registry+https://github.com/rust-lang/crates.io-index" 516 | checksum = "ad0f048c97dbd9faa9b7df56362b8ebcaa52adb06b498c050d2f4e32f90a7a8b" 517 | dependencies = [ 518 | "matchers", 519 | "nu-ansi-term", 520 | "once_cell", 521 | "regex", 522 | "sharded-slab", 523 | "smallvec", 524 | "thread_local", 525 | "tracing", 526 | "tracing-core", 527 | "tracing-log", 528 | ] 529 | 530 | [[package]] 531 | name = "unicode-ident" 532 | version = "1.0.12" 533 | source = "registry+https://github.com/rust-lang/crates.io-index" 534 | checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" 535 | 536 | [[package]] 537 | name = "utf8parse" 538 | version = "0.2.1" 539 | source = "registry+https://github.com/rust-lang/crates.io-index" 540 | checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" 541 | 542 | [[package]] 543 | name = "valuable" 544 | version = "0.1.0" 545 | source = "registry+https://github.com/rust-lang/crates.io-index" 546 | checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" 547 | 548 | [[package]] 549 | name = "winapi" 550 | version = "0.3.9" 551 | source = "registry+https://github.com/rust-lang/crates.io-index" 552 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 553 | dependencies = [ 554 | "winapi-i686-pc-windows-gnu", 555 | "winapi-x86_64-pc-windows-gnu", 556 | ] 557 | 558 | [[package]] 559 | name = "winapi-i686-pc-windows-gnu" 560 | version = "0.4.0" 561 | source = "registry+https://github.com/rust-lang/crates.io-index" 562 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 563 | 564 | [[package]] 565 | name = "winapi-x86_64-pc-windows-gnu" 566 | version = "0.4.0" 567 | source = "registry+https://github.com/rust-lang/crates.io-index" 568 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 569 | 570 | [[package]] 571 | name = "windows-sys" 572 | version = "0.52.0" 573 | source = "registry+https://github.com/rust-lang/crates.io-index" 574 | checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" 575 | dependencies = [ 576 | "windows-targets", 577 | ] 578 | 579 | [[package]] 580 | name = "windows-targets" 581 | version = "0.52.0" 582 | source = "registry+https://github.com/rust-lang/crates.io-index" 583 | checksum = "8a18201040b24831fbb9e4eb208f8892e1f50a37feb53cc7ff887feb8f50e7cd" 584 | dependencies = [ 585 | "windows_aarch64_gnullvm", 586 | "windows_aarch64_msvc", 587 | "windows_i686_gnu", 588 | "windows_i686_msvc", 589 | "windows_x86_64_gnu", 590 | "windows_x86_64_gnullvm", 591 | "windows_x86_64_msvc", 592 | ] 593 | 594 | [[package]] 595 | name = "windows_aarch64_gnullvm" 596 | version = "0.52.0" 597 | source = "registry+https://github.com/rust-lang/crates.io-index" 598 | checksum = "cb7764e35d4db8a7921e09562a0304bf2f93e0a51bfccee0bd0bb0b666b015ea" 599 | 600 | [[package]] 601 | name = "windows_aarch64_msvc" 602 | version = "0.52.0" 603 | source = "registry+https://github.com/rust-lang/crates.io-index" 604 | checksum = "bbaa0368d4f1d2aaefc55b6fcfee13f41544ddf36801e793edbbfd7d7df075ef" 605 | 606 | [[package]] 607 | name = "windows_i686_gnu" 608 | version = "0.52.0" 609 | source = "registry+https://github.com/rust-lang/crates.io-index" 610 | checksum = "a28637cb1fa3560a16915793afb20081aba2c92ee8af57b4d5f28e4b3e7df313" 611 | 612 | [[package]] 613 | name = "windows_i686_msvc" 614 | version = "0.52.0" 615 | source = "registry+https://github.com/rust-lang/crates.io-index" 616 | checksum = "ffe5e8e31046ce6230cc7215707b816e339ff4d4d67c65dffa206fd0f7aa7b9a" 617 | 618 | [[package]] 619 | name = "windows_x86_64_gnu" 620 | version = "0.52.0" 621 | source = "registry+https://github.com/rust-lang/crates.io-index" 622 | checksum = "3d6fa32db2bc4a2f5abeacf2b69f7992cd09dca97498da74a151a3132c26befd" 623 | 624 | [[package]] 625 | name = "windows_x86_64_gnullvm" 626 | version = "0.52.0" 627 | source = "registry+https://github.com/rust-lang/crates.io-index" 628 | checksum = "1a657e1e9d3f514745a572a6846d3c7aa7dbe1658c056ed9c3344c4109a6949e" 629 | 630 | [[package]] 631 | name = "windows_x86_64_msvc" 632 | version = "0.52.0" 633 | source = "registry+https://github.com/rust-lang/crates.io-index" 634 | checksum = "dff9641d1cd4be8d1a070daf9e3773c5f67e78b4d9d42263020c057706765c04" 635 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "boson" 3 | version = "0.3.0" 4 | edition = "2021" 5 | license = "MIT" 6 | authors = ["Pornpipat Popum "] 7 | publish = false 8 | description = "Run Electron Steam games natively" 9 | readme = "README.md" 10 | repository = "https://github.com/FyraLabs/boson" 11 | categories = [ 12 | "accessibility", 13 | "games", 14 | "wasm", 15 | "web-programming" 16 | ] 17 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 18 | 19 | [dependencies] 20 | clap = { version = "~4", features = ["derive"] } 21 | color-eyre = "~0.6" 22 | serde_json = "1.0.128" 23 | tracing = { version = "~0.1", features = ["log"] } 24 | tracing-subscriber = { version = "~0.3", features = ["env-filter"] } 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Cappy Ishihara, Fyra Labs 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | DESTDIR=$(PWD)/build 2 | CRATE_DIR=$(PWD) 3 | TARGET_DIR=$(CRATE_DIR)/target 4 | CRATE_NAME=boson 5 | CARGO_ARGS= 6 | CARGO_TARGET="release" 7 | 8 | .PHONY: build build-dev 9 | 10 | all: pack-release 11 | dev: pack-dev 12 | 13 | 14 | build: 15 | cargo build --release $(CARGO_ARGS) 16 | 17 | build-dev: 18 | cargo build 19 | 20 | pack-release: prep build 21 | cp $(TARGET_DIR)/$(CARGO_TARGET)/$(CRATE_NAME) $(DESTDIR)/$(CRATE_NAME) 22 | @echo "Copying assets" 23 | cp -av $(CRATE_DIR)/assets/. $(DESTDIR) 24 | pushd $(DESTDIR) && npm install && popd 25 | DEST_DIR=$(DESTDIR) scripts/build_steamworksjs.sh 26 | 27 | pack-dev: prep build-dev 28 | ln -fv $(TARGET_DIR)/debug/$(CRATE_NAME) $(DESTDIR)/$(CRATE_NAME) 29 | @echo "Copying assets" 30 | cp -av $(CRATE_DIR)/assets/. $(DESTDIR) 31 | 32 | prep: 33 | mkdir -p $(DESTDIR) 34 | 35 | clean: 36 | rm -rf $(DESTDIR) 37 | 38 | package: pack-release 39 | @# copy to tmp 40 | mkdir -p /tmp/$(CRATE_NAME) 41 | cp -av $(DESTDIR)/. /tmp/$(CRATE_NAME)/$(CRATE_NAME) 42 | tar -C /tmp/$(CRATE_NAME) -caf $(CRATE_NAME).tar.zst $(CRATE_NAME) 43 | rm -rf /tmp/$(CRATE_NAME) 44 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Boson ⚛️ 2 | 3 | Boson is a Steam compatibility tool that allows you to run Electron-based games with a native build of Electron, 4 | rather than using the game's bundled Electron version and running it through Proton. 5 | Think of it like [Boxtron], [Roberta], or [Luxtorpeda], but for Electron games. 6 | 7 | Please check [this list](https://steamdb.info/tech/Container/Electron/) to see if you need/want to use Boson for your game. 8 | 9 | [Boxtron]: https://github.com/dreamer/boxtron 10 | [Roberta]: https://github.com/dreamer/roberta 11 | [Luxtorpeda]: https://github.com/dreamer/luxtorpeda 12 | 13 | Inspired by [NativeCookie](https://github.com/Kesefon/NativeCookie/). 14 | 15 | This program should support almost all Electron-based games, including but not limited to: 16 | 17 | - [Cookie Clicker](https://store.steampowered.com/app/1454400/Cookie_Clicker) 18 | - [We Become What We Behold](https://store.steampowered.com/app/1103210/We_Become_What_We_Behold_FanMade_Port) 19 | - [natsuno-kanata](https://store.steampowered.com/app/1684660/natsunokanata__beyond_the_summer) 20 | 21 | ## Planned features 22 | 23 | - [ ] Database of tweaks for each supported game 24 | - [ ] Automatically set up Steamworks API for games that use it (see note above) 25 | - [ ] Download and install Electron builds from the Electron website (currently you need to bring your own Electron binary) 26 | - [ ] TOML configuration file(s) for custom tweaks 27 | - [ ] GUI for managing Boson (and displaying error messages) 28 | 29 | ## How does it work? 30 | 31 | Boson is a Steam Play compatibility tool, intercepting calls to run the game from its own executable, and redirects 32 | the executable to a native Electron executable, running the game using the provided Electron build instead. 33 | 34 | ## Why Boson? 35 | 36 | While some games are developed essentially as Electron PWAs, some game developers still refuse to publish native ports of their games. 37 | Expecting users to simply either run the game on Windows, or run its Electron executable under Proton. 38 | 39 | While this is a quick-and-dirty way to simply run Electron games on Linux, sometimes it may cause some issues due to the fact that we're running Chromium 40 | inside Proton, even though Electron has a native Linux build, e.g: 41 | 42 | - Graphical artifacts 43 | - Missing fonts (because the fonts are loaded exclusively from the Proton prefix) 44 | - Scaling issues 45 | - Other general compatibility issues 46 | 47 | Boson works around this issue entirely by simply just running the game using a native Electron 48 | build rather than running Electron inside Steam Proton. 49 | 50 | ## Usage 51 | 52 | 1. Install Electron from your package manager, or download the binaries from the [Electron website](https://www.electronjs.org/) (see note below), make sure the `electron` binary is in your `$PATH`. 53 | 2. Download the latest release tarball 54 | 3. Extract to `~/.steam/root/compatibilitytools.d/`. You should have a directory structure like this: 55 | 56 | ```sh 57 | ~/.steam/root/compatibilitytools.d/boson/ 58 | ├── boson 59 | ├── toolmanifest.vdf 60 | └── compatibiltytool.vdf 61 | ``` 62 | 63 | 4. Restart or start Steam if you haven't already 64 | 5. Right-click on the game you want to run with Boson, and select `Properties > Force the use of a specific Steam Play compatibility tool > Boson` 65 | 6. Run the game, And that's it! The game should now be running using the native Electron build. 66 | 67 | ## Building 68 | 69 | You require Rust and Node.js + NPM to build Boson. 70 | 71 | Install Rust by using [rustup](https://rustup.rs/), and then run the following commands: 72 | 73 | ```sh 74 | make 75 | ``` 76 | 77 | The resulting Steam compatibility tool will be outputted to `build/`. You can just copy the resulting files to `~/.steam/root/compatibilitytools.d/` and you're good to go. 78 | 79 | ## Notes 80 | 81 | - If you're using an electron binary that isn't in your $PATH and called `electron`, you can set the `ELECTRON_PATH` environment variable in your Steam launch options to point to the electron binary you want to use, e.g: 82 | 83 | ```sh 84 | ELECTRON_PATH=/path/to/electron %command% 85 | ``` 86 | 87 | - Due to some incompatibility issues with the Steam overlay, it's recommended to disable the Steam overlay for the game you're running with Boson. Boson is currently hardcoded to remove any `LD_PRELOAD` envars on runtime, to prevent the Steam overlay from being loaded. 88 | - Boson has a list of known ASAR paths that it checks through to find the game's files, if Boson somehow cannot find the electron ASAR assets path, you can set a custom environment variable to tell Boson where to find the game data, e.g: 89 | 90 | ```sh 91 | BOSON_LOAD_PATH=/path/to/asar %command% 92 | ``` 93 | 94 | ### Running Cookie Clicker (and other Greenworks games) with Boson 95 | 96 | This guide assumes you already bought Cookie Clicker on Steam, and have it installed. 97 | 98 | It also assumes that your CPU architecture is x86_64, and you're running a 64-bit Linux distribution, Steam for Linux only supports x86_64 for now. 99 | 100 | If you'd like to play the web version, just go to the [Cookie Clicker website](https://orteil.dashnet.org/cookieclicker/). 101 | The only differences between the web and Steam version is that the Steam version has cloud saves, Steam achievements, Workshop support, and an OST by C418 (Yes, the Minecraft guy). 102 | 103 | To get the Steamworks API to work with Cookie Clicker, you need to do the following: 104 | 105 | 1. Downloads the Steamworks SDK from the [Steamworks website](https://partner.steamgames.com/downloads/list) 106 | 2. Take note of these files from the SDK, we will move this to Greenwork's library location: 107 | - `sdk/redistributable_bin/linux64/libsteam_api.so` 108 | - `sdk/public/steam/lib/linux64/libsdkencryptedappticket.so` 109 | 3. Download the nightly builds of Greenworks for the respective compatible version of Electron from [here](https://greenworks-prebuilds.armaldio.xyz/), rename the resulting `.node` binary to `greenworks-linux64.node` 110 | 4. Once you downloaded the SDK, extract the SDK libraries to Cookie Clicker's installation directory, like this: 111 | 112 | ```txt 113 | ~/.local/share/Steam/steamapps/common/Cookie Clicker/ 114 | ├── resources 115 | |── app 116 | |── greenworks 117 | |── lib 118 | |──greenworks-linux64.node 119 | |──libsteam_api.so 120 | |──libsdkencryptedappticket.so 121 | |──(*libraries from other platforms*) 122 | ``` 123 | 124 | 5. Once you're done installing Greenworks, your copy of Cookie Clicker should now integrate with Steamworks, and you can now get achievements, cloud saves, and Workshop support as if you're still running the game on Windows, with the added benefit of Native Linux support (and Discord Rich Presence support) :3 125 | -------------------------------------------------------------------------------- /assets/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | package-lock.json 3 | -------------------------------------------------------------------------------- /assets/compatibilitytool.vdf: -------------------------------------------------------------------------------- 1 | "compatibilitytools" 2 | { 3 | "compat_tools" 4 | { 5 | "boson" // Internal name of this tool 6 | { 7 | "install_path" "." 8 | "display_name" "Boson" 9 | "from_oslist" "windows" 10 | "to_oslist" "linux" 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /assets/lib/.gitignore: -------------------------------------------------------------------------------- 1 | napi/* -------------------------------------------------------------------------------- /assets/lib/greenworks.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2015 Greenheart Games Pty. Ltd. All rights reserved. 2 | // Use of this source code is governed by the MIT license that can be 3 | // found in the LICENSE file. 4 | 5 | // The source code can be found in https://github.com/greenheartgames/greenworks 6 | 7 | // ============================== 8 | // Boson notes 9 | // ============================== 10 | // The following file is vendored from the greenworks project, slightly modified to work with Boson's Greenworks hooks. 11 | 12 | var fs = require('fs'); 13 | 14 | var greenworks; 15 | 16 | if (process.platform == 'darwin') { 17 | if (process.arch == 'x64') 18 | greenworks = require(__dirname + '/lib/greenworks-osx64'); 19 | } else if (process.platform == 'win32') { 20 | if (process.arch == 'x64') 21 | greenworks = require(__dirname + '/lib/greenworks-win64'); 22 | else if (process.arch == 'ia32') 23 | greenworks = require(__dirname + '/lib/greenworks-win32'); 24 | } else if (process.platform == 'linux') { 25 | if (process.arch == 'x64') 26 | greenworks = require(__dirname + '/lib/greenworks-linux64'); 27 | else if (process.arch == 'ia32') 28 | greenworks = require(__dirname + '/lib/greenworks-linux32'); 29 | } 30 | 31 | function error_process(err, error_callback) { 32 | if (err && error_callback) 33 | error_callback(err); 34 | } 35 | 36 | greenworks.ugcGetItems = function(options, ugc_matching_type, ugc_query_type, 37 | success_callback, error_callback) { 38 | if (typeof options !== 'object') { 39 | error_callback = success_callback; 40 | success_callback = ugc_query_type; 41 | ugc_query_type = ugc_matching_type; 42 | ugc_matching_type = options; 43 | options = { 44 | 'app_id': greenworks.getAppId(), 45 | 'page_num': 1 46 | } 47 | } 48 | greenworks._ugcGetItems(options, ugc_matching_type, ugc_query_type, 49 | success_callback, error_callback); 50 | } 51 | 52 | greenworks.ugcGetUserItems = function(options, ugc_matching_type, 53 | ugc_list_sort_order, ugc_list, success_callback, error_callback) { 54 | if (typeof options !== 'object') { 55 | error_callback = success_callback; 56 | success_callback = ugc_list; 57 | ugc_list = ugc_list_sort_order; 58 | ugc_list_sort_order = ugc_matching_type; 59 | ugc_matching_type = options; 60 | options = { 61 | 'app_id': greenworks.getAppId(), 62 | 'page_num': 1 63 | } 64 | } 65 | greenworks._ugcGetUserItems(options, ugc_matching_type, ugc_list_sort_order, 66 | ugc_list, success_callback, error_callback); 67 | } 68 | 69 | greenworks.ugcSynchronizeItems = function (options, sync_dir, success_callback, 70 | error_callback) { 71 | if (typeof options !== 'object') { 72 | error_callback = success_callback; 73 | success_callback = sync_dir; 74 | sync_dir = options; 75 | options = { 76 | 'app_id': greenworks.getAppId(), 77 | 'page_num': 1 78 | } 79 | } 80 | greenworks._ugcSynchronizeItems(options, sync_dir, success_callback, 81 | error_callback); 82 | } 83 | 84 | greenworks.publishWorkshopFile = function(options, file_path, image_path, title, 85 | description, success_callback, error_callback) { 86 | if (typeof options !== 'object') { 87 | error_callback = success_callback; 88 | success_callback = description; 89 | description = title; 90 | title = image_path; 91 | image_path = file_path; 92 | file_path = options; 93 | options = { 94 | 'app_id': greenworks.getAppId(), 95 | 'tags': [] 96 | } 97 | } 98 | greenworks._publishWorkshopFile(options, file_path, image_path, title, 99 | description, success_callback, error_callback); 100 | } 101 | 102 | greenworks.updatePublishedWorkshopFile = function(options, 103 | published_file_handle, file_path, image_path, title, description, 104 | success_callback, error_callback) { 105 | if (typeof options !== 'object') { 106 | error_callback = success_callback; 107 | success_callback = description; 108 | description = title; 109 | title = image_path; 110 | image_path = file_path; 111 | file_path = published_file_handle; 112 | published_file_handle = options; 113 | options = { 114 | 'tags': [] // No tags are set 115 | } 116 | } 117 | greenworks._updatePublishedWorkshopFile(options, published_file_handle, 118 | file_path, image_path, title, description, success_callback, 119 | error_callback); 120 | } 121 | 122 | // An utility function for publish related APIs. 123 | // It processes remains steps after saving files to Steam Cloud. 124 | function file_share_process(file_name, image_name, next_process_func, 125 | error_callback, progress_callback) { 126 | if (progress_callback) 127 | progress_callback("Completed on saving files on Steam Cloud."); 128 | greenworks.fileShare(file_name, function() { 129 | greenworks.fileShare(image_name, function() { 130 | next_process_func(); 131 | }, function(err) { error_process(err, error_callback); }); 132 | }, function(err) { error_process(err, error_callback); }); 133 | } 134 | 135 | // Publishing user generated content(ugc) to Steam contains following steps: 136 | // 1. Save file and image to Steam Cloud. 137 | // 2. Share the file and image. 138 | // 3. publish the file to workshop. 139 | greenworks.ugcPublish = function(file_name, title, description, image_name, 140 | success_callback, error_callback, progress_callback) { 141 | var publish_file_process = function() { 142 | if (progress_callback) 143 | progress_callback("Completed on sharing files."); 144 | greenworks.publishWorkshopFile(file_name, image_name, title, description, 145 | function(publish_file_id) { success_callback(publish_file_id); }, 146 | function(err) { error_process(err, error_callback); }); 147 | }; 148 | greenworks.saveFilesToCloud([file_name, image_name], function() { 149 | file_share_process(file_name, image_name, publish_file_process, 150 | error_callback, progress_callback); 151 | }, function(err) { error_process(err, error_callback); }); 152 | } 153 | 154 | // Update publish ugc steps: 155 | // 1. Save new file and image to Steam Cloud. 156 | // 2. Share file and images. 157 | // 3. Update published file. 158 | greenworks.ugcPublishUpdate = function(published_file_id, file_name, title, 159 | description, image_name, success_callback, error_callback, 160 | progress_callback) { 161 | var update_published_file_process = function() { 162 | if (progress_callback) 163 | progress_callback("Completed on sharing files."); 164 | greenworks.updatePublishedWorkshopFile(published_file_id, 165 | file_name, image_name, title, description, 166 | function() { success_callback(); }, 167 | function(err) { error_process(err, error_callback); }); 168 | }; 169 | 170 | greenworks.saveFilesToCloud([file_name, image_name], function() { 171 | file_share_process(file_name, image_name, update_published_file_process, 172 | error_callback, progress_callback); 173 | }, function(err) { error_process(err, error_callback); }); 174 | } 175 | 176 | // Greenworks Utils APIs implmentation. 177 | greenworks.Utils.move = function(source_dir, target_dir, success_callback, 178 | error_callback) { 179 | fs.rename(source_dir, target_dir, function(err) { 180 | if (err) { 181 | if (error_callback) error_callback(err); 182 | return; 183 | } 184 | if (success_callback) 185 | success_callback(); 186 | }); 187 | } 188 | 189 | greenworks.init = function() { 190 | if (this.initAPI()) return true; 191 | if (!this.isSteamRunning()) 192 | throw new Error("Steam initialization failed. Steam is not running."); 193 | var appId; 194 | try { 195 | appId = fs.readFileSync('steam_appid.txt', 'utf8'); 196 | } catch (e) { 197 | throw new Error("Steam initialization failed. Steam is running," + 198 | "but steam_appid.txt is missing. Expected to find it in: " + 199 | require('path').resolve('steam_appid.txt')); 200 | } 201 | if (!/^\d+ *\r?\n?$/.test(appId)) { 202 | throw new Error("Steam initialization failed. " + 203 | "steam_appid.txt appears to be invalid; " + 204 | "it should contain a numeric ID: " + appId); 205 | } 206 | throw new Error("Steam initialization failed, but Steam is running, " + 207 | "and steam_appid.txt is present and valid." + 208 | "Maybe that's not really YOUR app ID? " + appId.trim()); 209 | } 210 | 211 | var EventEmitter = require('events').EventEmitter; 212 | greenworks.__proto__ = EventEmitter.prototype; 213 | EventEmitter.call(greenworks); 214 | 215 | greenworks._steam_events.on = function () { 216 | greenworks.emit.apply(greenworks, arguments); 217 | }; 218 | 219 | process.versions['greenworks'] = greenworks._version; 220 | 221 | module.exports = greenworks; 222 | -------------------------------------------------------------------------------- /assets/lib/libsdkencryptedappticket.so: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FyraLabs/boson/17b256b5bf1b0a000e9d896a6c9d801069e00451/assets/lib/libsdkencryptedappticket.so -------------------------------------------------------------------------------- /assets/lib/libsteam_api.so: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FyraLabs/boson/17b256b5bf1b0a000e9d896a6c9d801069e00451/assets/lib/libsteam_api.so -------------------------------------------------------------------------------- /assets/lib/steamworksjs.js: -------------------------------------------------------------------------------- 1 | const { platform, arch } = process 2 | 3 | /** @typedef {typeof import('./client.d')} Client */ 4 | /** @type {Client} */ 5 | let nativeBinding = undefined 6 | 7 | console.log('platform:', platform) 8 | 9 | if (platform === 'win32' && arch === 'x64') { 10 | // nativeBinding = require('./dist/win64/steamworksjs.win32-x64-msvc.node') 11 | nativeBinding = require(__dirname + '/steamworksjs.win32-x64-msvc.node') 12 | } else if (platform === 'linux' && arch === 'x64') { 13 | // nativeBinding = require('./dist/linux64/steamworksjs.linux-x64-gnu.node') 14 | nativeBinding = require(__dirname + '/steamworksjs.linux-x64-gnu.node') 15 | } else if (platform === 'darwin') { 16 | if (arch === 'x64') { 17 | // nativeBinding = require('./dist/osx/steamworksjs.darwin-x64.node') 18 | nativeBinding = require(__dirname + '/steamworksjs.darwin-x64.node') 19 | } else if (arch === 'arm64') { 20 | // nativeBinding = require('./dist/osx/steamworksjs.darwin-arm64.node') 21 | nativeBinding = require(__dirname + '/steamworksjs.darwin-arm64.node') 22 | } 23 | } else { 24 | throw new Error(`Unsupported OS: ${platform}, architecture: ${arch}`) 25 | } 26 | 27 | let runCallbacksInterval = undefined 28 | 29 | /** 30 | * Initialize the steam client or throw an error if it fails 31 | * @param {number} [appId] - App ID of the game to load, if undefined, will search for a steam_appid.txt file 32 | * @returns {Omit} 33 | */ 34 | module.exports.init = (appId) => { 35 | const { init: internalInit, runCallbacks, restartAppIfNecessary, ...api } = nativeBinding 36 | 37 | internalInit(appId) 38 | 39 | clearInterval(runCallbacksInterval) 40 | runCallbacksInterval = setInterval(runCallbacks, 1000 / 30) 41 | 42 | return api 43 | } 44 | 45 | /** 46 | * @param {number} appId - App ID of the game to load 47 | * {@link https://partner.steamgames.com/doc/api/steam_api#SteamAPI_RestartAppIfNecessary} 48 | * @returns {boolean} 49 | */ 50 | module.exports.restartAppIfNecessary = (appId) => nativeBinding.restartAppIfNecessary(appId); 51 | 52 | /** 53 | * Enable the steam overlay on electron 54 | * @param {boolean} [disableEachFrameInvalidation] - Should attach a single pixel to be rendered each frame 55 | */ 56 | module.exports.electronEnableSteamOverlay = (disableEachFrameInvalidation) => { 57 | const electron = require('electron') 58 | if (!electron) { 59 | throw new Error('Electron module not found') 60 | } 61 | 62 | electron.app.commandLine.appendSwitch('in-process-gpu') 63 | electron.app.commandLine.appendSwitch('disable-direct-composition') 64 | 65 | if (!disableEachFrameInvalidation) { 66 | /** @param {electron.BrowserWindow} browserWindow */ 67 | const attachFrameInvalidator = (browserWindow) => { 68 | browserWindow.steamworksRepaintInterval = setInterval(() => { 69 | if (browserWindow.isDestroyed()) { 70 | clearInterval(browserWindow.steamworksRepaintInterval) 71 | } else if (!browserWindow.webContents.isPainting()) { 72 | browserWindow.webContents.invalidate() 73 | } 74 | }, 1000 / 60) 75 | } 76 | 77 | electron.BrowserWindow.getAllWindows().forEach(attachFrameInvalidator) 78 | electron.app.on('browser-window-created', (_, bw) => attachFrameInvalidator(bw)) 79 | } 80 | } 81 | 82 | const SteamCallback = nativeBinding.callback.SteamCallback 83 | module.exports.SteamCallback = SteamCallback -------------------------------------------------------------------------------- /assets/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "override-require": "^1.1.1" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /assets/register-hook.js: -------------------------------------------------------------------------------- 1 | // Hook to re-register the greenworks module 2 | 3 | console.log("BOSON: Hooking Greenworks"); 4 | 5 | console.debug("Attempting to override NAPI module path"); 6 | 7 | // const { pathToFileURL } = require("node:url"); 8 | const overrideRequire = require("override-require"); 9 | 10 | // https://github.com/ElectronForConstruct/greenworks-prebuilds/releases/download/v0.8.0/greenworks-electron-v125-linux-x64.node 11 | 12 | // We are going to do the hook twice 13 | 14 | // First hook pass: Replace the NAPI module path, so that it points to the correct NAPI module for the current Electron ABI 15 | 16 | function getElectronAbi() { 17 | const absoluteElectronPath = process.execPath; 18 | // try to run electron -a and get output 19 | const { execSync } = require("node:child_process"); 20 | const electronAbi = execSync(`${absoluteElectronPath} -a`) 21 | .toString() 22 | .trim(); 23 | console.debug("Electron ABI:", electronAbi); 24 | return electronAbi; 25 | } 26 | let napi; 27 | const napiDir = __dirname + "/lib/napi"; 28 | 29 | let napi_filename = `greenworks-electron-v${getElectronAbi()}-linux-x64.node`; 30 | 31 | let napiPath = `${napiDir}/${napi_filename}`; 32 | // let napiPath = `${napiDir}/greenworks-electron-v125-linux-x64.node`; 33 | 34 | // strip .node extension 35 | let napiPathNoExt = napiPath.replace(/\.node$/, ""); 36 | 37 | function http_get(url) { 38 | // recursive function that follows redirects 39 | const http = require("node:https"); 40 | const fs = require("node:fs"); 41 | 42 | const file = fs.createWriteStream(napiPath); 43 | 44 | const request = http.get(url, function (response) { 45 | if ( 46 | response.statusCode >= 300 && 47 | response.statusCode < 400 && 48 | response.headers.location 49 | ) { 50 | console.log("Following redirect to:", response.headers.location); 51 | http_get(response.headers.location); 52 | } else { 53 | response.pipe(file); 54 | } 55 | 56 | file.on("finish", function () { 57 | file.close(); 58 | console.log("Download complete."); 59 | console.log( 60 | "You may want to restart the game before Greenworks will work.", 61 | ); 62 | napi = require(napiPathNoExt); 63 | }); 64 | }); 65 | } 66 | 67 | function attempt_download_napi() { 68 | const http = require("node:https"); 69 | const fs = require("node:fs"); 70 | 71 | // check if file already exists 72 | fs.mkdirSync(napiDir, { recursive: true }); 73 | if (fs.existsSync(napiPath)) { 74 | console.log("NAPI file already exists. Skipping download."); 75 | return; 76 | } 77 | 78 | try { 79 | // recursively create the directory 80 | 81 | const url = `https://github.com/ElectronForConstruct/greenworks-prebuilds/releases/download/v0.8.0/${napi_filename}`; 82 | 83 | const file = fs.createWriteStream(napiPath); 84 | 85 | const request = http_get(url); 86 | } catch (e) { 87 | console.error("Failed to download NAPI file:", e); 88 | exit(1); 89 | } 90 | } 91 | 92 | attempt_download_napi(); 93 | 94 | try { 95 | console.log("Loading", napiPathNoExt); 96 | napi = require(napiPathNoExt); 97 | // console.log("Loaded NAPI module:", napi); 98 | } catch (e) { 99 | console.error(e); 100 | } 101 | 102 | const earlyOverride = (request, parent) => { 103 | // console.debug("PASS 1 CHECKING:", request); 104 | return request.includes("lib/greenworks-linux64"); 105 | }; 106 | 107 | const earlyResolve = (request, parent) => { 108 | console.debug("PASS 1 REQUEST:", request); 109 | return napi; 110 | }; 111 | 112 | // Early override pass 113 | 114 | overrideRequire(earlyOverride, earlyResolve); 115 | 116 | let greenworks; 117 | try { 118 | greenworks = require(__dirname + "/lib/greenworks"); 119 | } catch (e) { 120 | console.error(e); 121 | } 122 | 123 | // console.log("Greenworks:", greenworks); 124 | 125 | // === END FIRST PASS === 126 | 127 | // Second hook pass: Replace the actual game import for greenworks, so that it points to our custom greenworks module 128 | 129 | const isOverride = (request) => { 130 | console.debug("Trying to load", request); 131 | 132 | if (request.includes("greenworks/greenworks") 133 | || request.includes("steamworks.js") 134 | ) { 135 | console.debug("OVERRIDE:", request); 136 | return true 137 | } 138 | // return request.includes("steamworks.js"); 139 | return false; 140 | }; 141 | 142 | const resolveRequest = (request) => { 143 | console.debug("Parsing Request:", request); 144 | 145 | if (request.includes("steamworks.js")) { 146 | console.log("Returning steamworks.js loader", __dirname + "/lib/steamworksjs"); 147 | return require(__dirname + "/lib/steamworksjs"); 148 | } 149 | 150 | if (request.includes("greenworks/greenworks")) { 151 | console.log("Returning Greenworks module", greenworks); 152 | return greenworks; 153 | } 154 | 155 | // return greenworks; 156 | }; 157 | 158 | overrideRequire(isOverride, resolveRequest); 159 | -------------------------------------------------------------------------------- /assets/toolmanifest.vdf: -------------------------------------------------------------------------------- 1 | "manifest" 2 | { 3 | "commandline" "/boson run" 4 | "commandline_waitforexitandrun" "/boson run" 5 | "commandline_getnativepath" "/boson path" 6 | "commandline_getcompatpath" "/boson path" 7 | "compatmanager_layer_name" "container-runtime" 8 | "require_tool_appid" "1391110" 9 | } 10 | -------------------------------------------------------------------------------- /justfile: -------------------------------------------------------------------------------- 1 | set dotenv-load 2 | 3 | run: 4 | make pack-dev 5 | $PWD/build/boson run -- "$GAME_PATH" $GAME_ARGS -------------------------------------------------------------------------------- /scripts/build_steamworksjs.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -x 2 | 3 | DEST_DIR="${DEST_DIR:-$PWD/build}" 4 | echo "DEST_DIR: ${DEST_DIR}" 5 | 6 | # This script builds steamworks.js for the current platform. 7 | 8 | # It assumes that you have NPM and the Rust toolchain installed. 9 | 10 | GIT_REPO="https://github.com/ceifa/steamworks.js.git" 11 | 12 | TMP_DIR="$(mktemp -d)/steamworks.js" 13 | DIST_DIR="${TMP_DIR}/dist" 14 | 15 | function clone_repo { 16 | git clone "${GIT_REPO}" "${TMP_DIR}" 17 | } 18 | 19 | function build { 20 | pushd "${TMP_DIR}" 21 | npm install 22 | npm run build 23 | } 24 | 25 | function copy_dist { 26 | mkdir -p "${DIST_DIR}" 27 | cp -r "${TMP_DIR}/dist/linux64/"*.node "${DEST_DIR}/lib/" 28 | } 29 | 30 | clone_repo 31 | 32 | build 33 | 34 | copy_dist -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | use clap::{Parser, Subcommand}; 2 | use color_eyre::{eyre::OptionExt, Result}; 3 | use path_search::get_asar_path; 4 | use std::{path::PathBuf, process::Command}; 5 | mod path_search; 6 | // use tracing_subscriber::; 7 | #[cfg(not(debug_assertions))] 8 | const DEFAULT_LOG_LEVEL: &str = "info"; 9 | #[cfg(debug_assertions)] 10 | const DEFAULT_LOG_LEVEL: &str = "trace"; 11 | 12 | #[derive(Parser)] 13 | #[command(version, about, long_about = None)] 14 | #[command(propagate_version = true)] 15 | #[command(allow_hyphen_values = true)] 16 | pub struct Boson { 17 | #[command(subcommand)] 18 | cmd: Commands, 19 | } 20 | #[derive(Subcommand)] 21 | pub enum Commands { 22 | #[command(alias = "waitforexitandrun")] 23 | Run { 24 | game_path: PathBuf, 25 | // do not parse any further, treat all further arguments here as just vec of strings 26 | // e.g unknown args get added here 27 | #[arg(trailing_var_arg = true)] 28 | #[arg(allow_hyphen_values = true)] 29 | additional_args: Vec, 30 | }, 31 | 32 | Path { 33 | path: PathBuf, 34 | }, 35 | } 36 | 37 | fn main() -> Result<()> { 38 | color_eyre::install().unwrap(); 39 | 40 | tracing_subscriber::fmt() 41 | .pretty() 42 | .with_env_filter(DEFAULT_LOG_LEVEL) 43 | .init(); 44 | 45 | let appname = env!("CARGO_PKG_NAME"); 46 | let appversion = env!("CARGO_PKG_VERSION"); 47 | // print the args 48 | tracing::info!("{appname} {appversion} starting up, logging at {DEFAULT_LOG_LEVEL} level."); 49 | tracing::info!( 50 | "Launched with args: {:?}", 51 | std::env::args().collect::>() 52 | ); 53 | let args = Boson::parse(); 54 | let exec_path = std::env::current_exe()?; 55 | 56 | // get the folder of the executable 57 | let exec_dir = exec_path.parent().unwrap(); 58 | 59 | tracing::info!("Executable path: {:?}", exec_path); 60 | tracing::info!("Executable directory: {:?}", exec_dir); 61 | 62 | // Create path for hook 63 | let hook_path = exec_dir.join("register-hook.js"); 64 | 65 | match args.cmd { 66 | Commands::Run { 67 | game_path, 68 | additional_args, 69 | } => { 70 | let electron = path_search::env_electron_path(); 71 | 72 | let mut args = vec!["--no-sandbox"]; 73 | 74 | let gpath = path_search::get_game_path(&game_path); 75 | // Actually get the game executable path here 76 | let app_path_str = get_asar_path(&game_path).ok_or_eyre( 77 | "Could not find ASAR file in game directory. Make sure you're running this from the game directory.", 78 | )?; 79 | 80 | // todo: path to boson hook 81 | let load_hook_arg = vec!["--require", hook_path.to_str().unwrap()]; 82 | 83 | // Add the args before the app path 84 | args.extend(load_hook_arg.iter()); 85 | args.extend(additional_args.iter().map(|s| s.as_str())); 86 | args.push(app_path_str.to_str().unwrap()); 87 | 88 | tracing::info!(?gpath); 89 | 90 | tracing::debug!(?args); 91 | 92 | // Remove steam overlay from LD_PRELOAD 93 | 94 | let ld_preload = std::env::var("LD_PRELOAD").unwrap_or_default(); 95 | // shadow the variable 96 | // 97 | // filter out the gameoverlayrenderer 98 | let ld_preload = std::env::split_paths(&ld_preload) 99 | .filter(|x| { 100 | x.to_str() 101 | .map(|x| !x.contains("gameoverlayrenderer")) 102 | .unwrap_or(true) 103 | }) 104 | .collect::>(); 105 | 106 | let ld_preload = std::env::join_paths(ld_preload).unwrap(); 107 | 108 | let ld_library_path = std::env::var("LD_LIBRARY_PATH").unwrap_or_default(); 109 | let mut ld_library_path = std::env::split_paths(&ld_library_path).collect::>(); 110 | 111 | // add the exec_dir/lib to the LD_LIBRARY_PATH 112 | 113 | ld_library_path.push(exec_dir.join("lib")); 114 | 115 | let ld_library_path = std::env::join_paths(ld_library_path).unwrap(); 116 | 117 | let mut cmd = Command::new(electron); 118 | cmd.current_dir(&gpath) 119 | .env("LD_LIBRARY_PATH", ld_library_path) 120 | // Do not preload any libraries, hack to fix Steam overlay 121 | .env("LD_PRELOAD", ld_preload) 122 | .args(args); 123 | 124 | let c = cmd.spawn()?.wait(); 125 | 126 | if let Err(e) = c { 127 | return Err(color_eyre::eyre::eyre!(e)); 128 | }; 129 | Ok(()) 130 | } 131 | Commands::Path { path } => { 132 | println!("{}", path_search::get_game_path(&path).display()); 133 | Ok(()) 134 | } 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /src/path_search.rs: -------------------------------------------------------------------------------- 1 | //! Path Searching module 2 | //! 3 | //! This module is a helper to quickly find the path to the Electron app's ASAR file by looking for them in common locations. 4 | //! 5 | //! It also supports checking the environment variable `BOSON_LOAD_PATH` for a custom path. 6 | use std::path::{Path, PathBuf}; 7 | 8 | pub fn env_boson_load_path() -> Option { 9 | std::env::var("BOSON_LOAD_PATH").ok() 10 | } 11 | 12 | pub fn get_game_path(path: &Path) -> PathBuf { 13 | // remove file name from path 14 | if path.is_file() { 15 | path.parent().unwrap().canonicalize().unwrap().to_path_buf() 16 | } else { 17 | path.canonicalize().unwrap().to_path_buf() 18 | } 19 | } 20 | 21 | pub fn env_electron_path() -> String { 22 | std::env::var("ELECTRON_PATH").unwrap_or_else(|_| "electron".to_string()) 23 | } 24 | 25 | // This function scans for a package.json file in the game directory 26 | // Does nothing for now except logging 27 | 28 | fn package_json_scan(path: &Path) { 29 | use serde_json::Value; 30 | use std::fs::File; 31 | use std::io::Read; 32 | 33 | // if is file 34 | if path.is_file() { 35 | tracing::info!("Path is a file, ignoring."); 36 | return; 37 | } 38 | 39 | let package_json = path.join("package.json"); 40 | if package_json.exists() { 41 | tracing::info!("Found package.json at {:?}", package_json); 42 | 43 | // Try to open the file 44 | let mut file = match File::open(&package_json) { 45 | Ok(file) => file, 46 | Err(e) => { 47 | tracing::warn!("Failed to open package.json: {:?}", e); 48 | return; 49 | } 50 | }; 51 | 52 | let mut contents = String::new(); 53 | if let Err(e) = file.read_to_string(&mut contents) { 54 | tracing::warn!("Failed to read package.json: {:?}", e); 55 | return; 56 | } 57 | 58 | let v: Value = match serde_json::from_str(&contents) { 59 | Ok(v) => v, 60 | Err(e) => { 61 | tracing::warn!("Failed to parse package.json: {:?}", e); 62 | return; 63 | } 64 | }; 65 | 66 | // Check if it's a valid Electron package 67 | if v.get("main").is_none() { 68 | tracing::warn!("package.json does not specify a main script, it may not be a valid Electron package."); 69 | } else { 70 | tracing::info!("Validated package.json as an Electron package"); 71 | } 72 | } else { 73 | tracing::warn!( 74 | "Could not find package.json in game directory. This may not be the game directory." 75 | ); 76 | } 77 | } 78 | 79 | fn _compat_data_path() -> Option { 80 | std::env::var("STEAM_COMPAT_DATA_PATH") 81 | .ok() 82 | .map(|s| PathBuf::from(s).join("boson")) 83 | .map(|p| p.to_str().unwrap().to_string()) 84 | } 85 | 86 | /// Get ASAR path 87 | /// 88 | /// Accepts a game root directory, usually from `get_game_path()` 89 | /// and returns the path to the ASAR 90 | #[tracing::instrument] 91 | pub fn get_asar_path(game_exec_path: &Path) -> Option { 92 | let game_path = { 93 | if let Ok(path) = std::env::var("STEAM_COMPAT_INSTALL_PATH") { 94 | tracing::info!("STEAM_COMPAT_INSTALL_PATH found: {:?}", path); 95 | path.into() 96 | } 97 | // If the game path is not provided, use the game executable path 98 | else { 99 | get_game_path(game_exec_path) 100 | } 101 | }; 102 | 103 | tracing::trace!("Game path: {:?}", game_path); 104 | // First check if there's an override in the environment 105 | if let Some(path) = env_boson_load_path() { 106 | return Some(game_path.join(path)); 107 | } 108 | 109 | // ASAR paths priority 110 | // 111 | // Unpacked folders are prioritized over ASAR files 112 | // as games may be unpacked for development or modding. 113 | // 114 | // todo: walk through every directory in the actual game path (except node_modules) and find a package.json with "main" pointing to the JS file 115 | // if found, return that directory as the game path 116 | // else find the ASAR archive 117 | 118 | const ASAR_PATHS: [&str; 4] = [ 119 | "app.asar", 120 | "resources/app.asar.unpacked", 121 | "resources/app.asar", 122 | "resources/app", 123 | ]; 124 | 125 | // Funny guard clause 126 | 127 | // If the current game_path is actually one of (ends with) the actual ASAR path here, return it directly 128 | if ASAR_PATHS.iter().any(|path| game_path.ends_with(path)) { 129 | // find package.json 130 | package_json_scan(&game_path); 131 | tracing::info!("Found ASAR at {:?}", game_path); 132 | return Some(game_path); 133 | } 134 | 135 | for path in ASAR_PATHS.iter() { 136 | let asar_path = game_path.join(path); 137 | tracing::trace!("Checking path: {:?}", asar_path); 138 | if asar_path.exists() { 139 | if asar_path.is_dir() { 140 | tracing::info!("Found unpacked ASAR at {:?}", asar_path); 141 | } else { 142 | tracing::info!("Found ASAR at {:?}", asar_path); 143 | } 144 | package_json_scan(&asar_path); 145 | return Some(asar_path); 146 | } 147 | } 148 | 149 | None 150 | } 151 | --------------------------------------------------------------------------------