118 Commits

Author SHA1 Message Date
SaltySnail
642dc76a92 Remove backend 2026-03-28 22:23:33 +01:00
Tixx
b1c3b978ff Fix include order for winsock2 2026-01-24 20:34:47 +01:00
Lion Kortlepel
7d00d489e4 remove invalid flag 2026-01-24 20:09:37 +01:00
Lion Kortlepel
fd398ed5ab add new header implementation for game<->launcher communication 2026-01-24 20:09:37 +01:00
Lion Kortlepel
b4f0f0759d flush all prints 2026-01-24 20:09:37 +01:00
Lion Kortlepel
5b2bc2d499 print posix_spawn errors 2026-01-24 20:09:26 +01:00
Tixx
fab9ff5559 Provide more info if the launcher update fails (#214)
Also prevents the launcher from bricking itself (again)

---

By creating this pull request, I understand that code that is AI
generated or otherwise automatically generated may be rejected without
further discussion.
I declare that I fully understand all code I pushed into this PR, and
wrote all this code myself and own the rights to this code.
2025-11-22 20:09:15 +01:00
Tixx
a2789a8524 Bump version to v2.7.0 2025-10-20 22:39:36 +02:00
Tixx
3db98eaf0c Provide more info if the launcher update fails
Also prevents the launcher from bricking itself (again)
2025-10-05 15:13:49 +02:00
Tixx
6f84b56f1b Bump version 2025-09-27 20:47:14 +02:00
Tixx
6f5197217c Workaround bom header (#211)
This PR removes the utf8 bom encoding header which was causing issues.
This PR also adds support for unicode characters in the terminal on
windows.

For context it looks like the BeamNG launcher only encodes it with the
utf8 header, which is what it's currently removing.

---

By creating this pull request, I understand that code that is AI
generated or otherwise automatically generated may be rejected without
further discussion.
I declare that I fully understand all code I pushed into this PR, and
wrote all this code myself and own the rights to this code.
2025-09-27 20:14:10 +02:00
Tixx
d1d2b3414b Adjust ini parser (#212)
This PR removes mid-line ini comments which were causing issue because
users had `#` and `;` in their file path.

---

By creating this pull request, I understand that code that is AI
generated or otherwise automatically generated may be rejected without
further discussion.
I declare that I fully understand all code I pushed into this PR, and
wrote all this code myself and own the rights to this code.
2025-09-27 20:08:13 +02:00
Tixx
a669557726 Make logger stdout cross platform again 2025-09-21 21:54:24 +02:00
Tixx
be1f5c04f7 Remove mid-line ini comments 2025-09-21 21:49:40 +02:00
Tixx
1209ff88e2 Support unicode in windows terminal 2025-09-21 21:39:57 +02:00
Tixx
c40af681bf Strip bom encoding 2025-09-21 21:39:38 +02:00
Tixx
c03b1d5946 Bump version to v2.6.3 2025-09-17 09:20:21 +02:00
Tixx
5e73c7bce2 Check outdated registry entry for GameDir 2025-09-17 09:19:50 +02:00
Tixx
386f471362 Fix includes for linux build 2025-09-16 20:16:28 +02:00
Tixx
6c3bfda23b Bump version to v2.6.2 2025-09-16 20:09:56 +02:00
Tixx
5737e27bf3 Use ini instead of registry for game dir 2025-09-16 20:07:57 +02:00
Tixx
1860c0aef1 Fix userfolder on linux (hopefully) 2025-09-16 20:07:26 +02:00
Tixx
9d20b678f9 Bump version to v2.6.1 2025-09-16 17:53:29 +02:00
Tixx
be7594039e Fix updating logic 2025-09-16 17:52:30 +02:00
Tixx
77f3375658 Bump version to v2.6.0 2025-09-16 16:38:55 +02:00
Tixx
ae6e5b51bf User folder parsing updates (#208)
This PR adds support for the changes made to the BeamNG userfolder path
location.
1. Registry check for userfolder removed
2. New ini file is checked for userfolder
3. Ini parser now supports global keys

---

By creating this pull request, I understand that code that is AI
generated or otherwise automatically generated may be rejected without
further discussion.
I declare that I fully understand all code I pushed into this PR, and
wrote all this code myself and own the rights to this code.
2025-09-16 16:36:07 +02:00
Tixx
8a0f87f476 Remove registry check and add support for new ini file 2025-09-16 16:11:26 +02:00
Tixx
33b2030f97 Support global keys in ini 2025-09-16 16:10:16 +02:00
Tixx
5b2eb0a1a0 Change to execution paths to binary paths (#200)
Previously, it used argv[0] for the execution path, which is fine in
most cases, but in my case, it is not.
The thing is, I use NixOS, and I have created a Nix module for BeamMP.
When running BeamMP-Launcher, it tries to check for updates, and fails
because of the SHA hash.
There are other ways around this, such as setting the execution
directory, but I think this is a far clearer approach

---

By creating this pull request, I understand that code that is AI
generated or otherwise automatically generated may be rejected without
further discussion.
I declare that I fully understand all code I pushed into this PR, and
wrote all this code myself and own the rights to this code.
2025-08-02 19:38:18 +02:00
Vali0004
f27d3c6120 Fix bug with BP and not having a trailing slash by default 2025-08-02 00:21:40 -04:00
Vali0004
7a4b24d616 Change to canonical paths for executable paths 2025-08-02 00:14:37 -04:00
Tixx
b6b0e4ba3e Refine updating (#201)
This PR makes the launcher update more reliable by downloading the
update to a "new_BeamMP-Launcher.exe" file, instead of the launcher
renaming itself to a backup (effectively bricking itself and the
shortcut) and then hoping the download doesn't fail.

---

By creating this pull request, I understand that code that is AI
generated or otherwise automatically generated may be rejected without
further discussion.
I declare that I fully understand all code I pushed into this PR, and
wrote all this code myself and own the rights to this code.
2025-07-27 01:58:06 +02:00
Tixx
a87aa7230a Make null termination in hex encoding more clear 2025-07-27 01:07:23 +02:00
Tixx
b9cc025083 Check hashes after downloading 2025-07-25 23:50:16 +02:00
Tixx
c0ed056440 Fix for backup failing when it's in use
The backup file may still be in use by older launcher versions (v2.0.71) if they update
2025-07-17 14:33:25 +02:00
Tixx
e6e5bf8327 Fix launcher update to delete backup after download 2025-07-17 14:32:17 +02:00
Tixx
e7cfb6e406 Add Debian legacy Steam installation path (#198)
Current Steam installations seem to have a much cleaner filesystem
layout on Debian while older ones look quite different.

Older Debian installations ~/.steam/root points to ~/.steam while newer
ones point to ~/.steam/debian-installation and hence BeamMP cannot find
integrity.json. 'steamapps' is at ~/.steam/debian-installation in newer
installations.

On the older installations, 'steamapps' is at ~/.steam/steam/steamapps.

---

By creating this pull request, I understand that code that is AI
generated or otherwise automatically generated may be rejected without
further discussion.
I declare that I fully understand all code I pushed into this PR, and
wrote all this code myself and own the rights to this code.
2025-07-09 11:12:11 +02:00
Markus Ingalsuo
185818d174 Add Debian legacy Steam installation path
Current Steam installations seem to have a much cleaner filesystem
layout on Debian while older ones look quite different.

Older Debian installations ~/.steam/root points to ~/.steam while newer
ones point to ~/.steam/debian-installation and hence BeamMP cannot
find integrity.json. 'steamapps' is at ~/.steam/debian-installation in
newer installations.

On the older installations, 'steamapps' is at ~/.steam/steam/steamapps.

Signed-off-by: Markus Ingalsuo <markus.ingalsuo@gmail.com>
2025-07-09 11:34:45 +03:00
Tixx
406c79ef82 Bump version to v2.5.1 2025-07-08 13:24:32 +02:00
Tixx
f104451bb9 Switch Invalid INI line log to debug (#196)
By creating this pull request, I understand that code that is AI
generated or otherwise automatically generated may be rejected without
further discussion.
I declare that I fully understand all code I pushed into this PR, and
wrote all this code myself and own the rights to this code.
2025-07-05 20:24:43 +02:00
Tixx
d52def2114 Make new CheckVer & GetEN compatible with linux (#195)
Fixes issues for linux that came from PR #167 

---

By creating this pull request, I understand that code that is AI
generated or otherwise automatically generated may be rejected without
further discussion.
I declare that I fully understand all code I pushed into this PR, and
wrote all this code myself and own the rights to this code.
2025-07-03 23:29:07 +02:00
Tixx
d71757b56c Switch Invalid INI line log to debug 2025-06-29 12:56:24 +02:00
Tixx
f7d3fcf925 Make new CheckVer & GetEN compatible with linux 2025-06-29 10:07:34 +02:00
Tixx
7bef6f35c2 Bump version to 2.5.0 2025-06-28 20:21:17 +02:00
Tixx
b64d645f73 Switch to wstring for paths on windows (#167)
This PR changes everything relating to file system paths to use
std::wstring instead of std::string. This allows for non-latin
characters to be in the user's path, for example in their windows
username.

- [x] Convert windows code to use wstring
- [x] Fix linux build
- [x] Fix hashing

---

By creating this pull request, I understand that code that is AI
generated or otherwise automatically generated may be rejected without
further discussion.
I declare that I fully understand all code I pushed into this PR, and
wrote all this code myself and own the rights to this code.
2025-06-28 19:25:13 +02:00
Tixx
1780133569 Include assert 2025-06-25 14:29:29 +02:00
Tixx
c89afdf477 Check
Co-authored-by: SaltySnail <51403141+SaltySnail@users.noreply.github.com>
2025-06-24 22:17:14 +02:00
Tixx
9d44146224 Regex assert
Co-authored-by: SaltySnail <51403141+SaltySnail@users.noreply.github.com>
2025-06-24 22:16:17 +02:00
Tixx
a5c02217fa Update ini parse check formatting
Co-authored-by: SaltySnail <51403141+SaltySnail@users.noreply.github.com>
2025-06-20 23:04:07 +02:00
Tixx
303fc55d94 Fix syntax error 2025-06-19 18:13:46 +02:00
Tixx
5f1e7c6409 Fix resources dir log message for linux 2025-06-19 18:07:59 +02:00
Tixx
8025c0884f Fix download path generation 2025-06-19 17:56:59 +02:00
Tixx
51d096deac Check if BeamMP.zip exists before hashing 2025-06-08 14:01:48 +02:00
Tixx
9c53f86593 Convert GetGamePath() to fs::path 2025-06-08 14:01:23 +02:00
Tixx
e0257e9526 Update NewSyncResources logs to use wstring 2025-06-08 13:51:16 +02:00
Tixx
8b96ffb098 Fix .git folder check and GetGamePath() 2025-06-08 13:45:09 +02:00
Tixx
6c740e2562 Fix merge 2025-06-08 13:34:15 +02:00
Tixx
06c741edc5 Fix wstring for windows and add crossplatform code for fs strings 2025-06-08 13:34:15 +02:00
Tixx
5e448dc34f Switch to wstring for paths on windows 2025-06-08 13:34:15 +02:00
Tixx
676084f283 Implement DeleteDuplicateMods option (#190)
Adds `DeleteDuplicateMods` option to the launcher config which, well,
deletes mods with the same name if their hashes mismatch. Useful for
development where client mod hashes can frequently change.

---

By creating this pull request, I understand that code that is AI
generated or otherwise automatically generated may be rejected without
further discussion.
I declare that I fully understand all code I pushed into this PR, and
wrote all this code myself and own the rights to this code.
2025-06-08 12:56:23 +02:00
Tixx
a8cd208208 Make duplicate mod detection more readable
Co-authored-by: SaltySnail <51403141+SaltySnail@users.noreply.github.com>
2025-06-08 12:37:35 +02:00
Tixx
2529146d5a Implement DeleteDuplicateMods option 2025-06-08 12:37:32 +02:00
Tixx
8d641f326d Check if unpacked BeamMP has a .git folder before deleting it (#193)
This PR makes it so that the launcher will first check for the presence
of a .git folder before deleting the unpacked BeamMP directory.
This is useful when you're working on the BeamMP mod and you
accidentally start the launcher without dev mode. (This has happened to
me more times than I would like to admit.)

---

By creating this pull request, I understand that code that is AI
generated or otherwise automatically generated may be rejected without
further discussion.
I declare that I fully understand all code I pushed into this PR, and
wrote all this code myself and own the rights to this code.
2025-06-07 20:07:23 +02:00
Tixx
5af9f5da36 Show cached mods in download progress (#189)
This PR adds cached mods to the "Loading resources" pop-up in-game and
makes it so that the count is actually right.
This PR also fixes the mod protection message because for some reason I
forced it to be always off 😁.

---

By creating this pull request, I understand that code that is AI
generated or otherwise automatically generated may be rejected without
further discussion.
I declare that I fully understand all code I pushed into this PR, and
wrote all this code myself and own the rights to this code.
2025-06-07 20:05:52 +02:00
Tixx
baba0ad026 Check if unpacked BeamMP has a .git folder before deleting it 2025-06-07 17:50:20 +02:00
Tixx
b1ebcfc18d Fix mod protection message 2025-05-27 22:12:07 +02:00
Tixx
187ef3b24f Show cached mods being loaded in download progress 2025-05-27 22:09:52 +02:00
Tixx
943889d588 Fix mod downloading progress 2025-05-27 22:08:50 +02:00
Tixx
edbd99f389 Added CLI argument for user-path (#148)
Simple addition to the CLI args. It could probably do with some
validation, ie; making sure it ends in a slash.

cc: @WiserTixx
2025-05-05 23:47:11 +02:00
Tixx
0341d401e7 Download check (#184)
This PR makes it so that the mod hash and download confirmation are
verified before proceeding.

---

By creating this pull request, I understand that code that is AI
generated or otherwise automatically generated may be rejected without
further discussion.
I declare that I fully understand all code I pushed into this PR, and
wrote all this code myself and own the rights to this code.
2025-05-05 23:46:32 +02:00
Tixx
da84d62391 Notify user about missing protected mods (#183)
Launcher implementation for
https://github.com/BeamMP/BeamMP-Server/pull/430. This PR checks if mods
are protected, and if a mod is missing the launcher will notify the user
about it and give them instructions on how to resolve it.

---

By creating this pull request, I understand that code that is AI
generated or otherwise automatically generated may be rejected without
further discussion.
I declare that I fully understand all code I pushed into this PR, and
wrote all this code myself and own the rights to this code.
2025-05-05 23:46:14 +02:00
Tixx
cc6167cd2e Bump version 2025-05-03 22:20:55 +02:00
Tixx
d3263acead Log corrupted download confirmation 2025-05-01 20:54:28 +02:00
Tixx
4de0bc9a40 Check download confirmation packet 2025-05-01 20:47:16 +02:00
Tixx
25a1061700 Verify mod hash after downloading 2025-05-01 20:42:01 +02:00
Tixx
f193c25de6 Switch to using INI parser for startup.ini file 2025-04-27 23:35:39 +02:00
Tixx
efe9f5b614 Add INI parser and function to expand env vars 2025-04-27 23:35:11 +02:00
Tixx
6244fdafc6 Use std::filesystem::operator/ instead of string concat 2025-04-26 19:45:10 +02:00
Tixx
ca93effb7d Update --user-path error message
Co-authored-by: Lion <development@kortlepel.com>
2025-04-26 18:46:15 +02:00
Tixx
f9d347bd9b Look for a userfolder specficied in the game's ini config 2025-04-26 18:42:41 +02:00
Tixx
a63f1bd27c Check if user path argument exists 2025-04-26 18:42:40 +02:00
Tyler Hoyt
ffc36e7f3d Added cli option for user-path 2025-04-26 18:42:31 +02:00
SaltySnail
e216b6ec06 Get error code when the game fails to launch on windows (#179)
This PR logs the error `CreateProcessA` returns when failing on windows.
This can help with figuring out why the launcher `Failed to Launch the
game!`

---

By creating this pull request, I understand that code that is AI
generated or otherwise automatically generated may be rejected without
further discussion.
I declare that I fully understand all code I pushed into this PR, and
wrote all this code myself and own the rights to this code.
2025-04-19 21:43:41 +02:00
Tixx
6597fe5e26 Rename windows api variable 2025-04-19 21:23:51 +02:00
Tixx
2fa5d69369 Link to the docs (#181)
Link to the docs instead

---

By creating this pull request, I understand that code that is AI
generated or otherwise automatically generated may be rejected without
further discussion.
I declare that I fully understand all code I pushed into this PR, and
wrote all this code myself and own the rights to this code.
2025-04-05 12:45:22 +02:00
O1LER
fec80e2c67 Link to the docs 2025-04-05 12:04:57 +02:00
Tixx
dc78883451 Notify user about missing protected mods 2025-04-01 09:09:49 +02:00
Tixx
fa8627a22b Fix recv return type and better download error handling (#178)
Fixes the recv return value type. This PR corrects the recv return value
type from `int32_t` to `int`. Casting the return value from `int` to
`int32_t` (Currently the case, changed by this pr) would in some cases,
if the transmitted packet was large enough, flip the value causing it to
be a high negative number, which recv will never return. This happens
frequently when downloading big mods over a fast connection.

---

By creating this pull request, I understand that code that is AI
generated or otherwise automatically generated may be rejected without
further discussion.
I declare that I fully understand all code I pushed into this PR, and
wrote all this code myself and own the rights to this code.
2025-03-29 20:18:19 +01:00
Tixx
dd5256ae22 Increase download speed calculation precision 2025-03-29 00:19:25 +01:00
Tixx
e24cbf61bb Only fail on socket error or connection closed 2025-03-29 00:19:24 +01:00
Tixx
472e2d16b6 Fix recv return type and better download error handling 2025-03-29 00:19:24 +01:00
Tixx
ae650cc142 Get error code when the game fails to launch on windows 2025-03-28 23:16:37 +01:00
Tixx
ad7177bec8 Include chrono (#176)
By creating this pull request, I understand that code that is AI
generated or otherwise automatically generated may be rejected without
further discussion.
I declare that I fully understand all code I pushed into this PR, and
wrote all this code myself and own the rights to this code.
2025-03-23 22:38:20 +01:00
Tixx
a4005c5876 Include chrono 2025-03-15 23:06:49 +01:00
Tixx
a3ad6f8700 Properly handle the futures (#172)
This PR makes it so that the std::async calls are actually asynchronous.

---

By creating this pull request, I understand that code that is AI
generated or otherwise automatically generated may be rejected without
further discussion.
I declare that I fully understand all code I pushed into this PR, and
wrote all this code myself and own the rights to this code.
2025-03-08 22:29:08 +01:00
Tixx
d3bddb0203 Properly handle the future 2025-02-23 22:04:15 +01:00
Tixx
9e93fa35fa Bump version 2025-01-21 22:36:40 +01:00
Tixx
8373a70c4b Mod download improvements (#162)
This PR adds multiple features relating to mod downloads.
1. With this PR the launcher will convert old style mods (without hash)
to new style mods (with hash). This is so people don't have to
re-download all of their mods after joining a server which was
previously on a version below 3.6.0
2. This PR will save the last used date of mods in a JSON file in the
caching directory. This will allow for smart deletion of cached mods

---

By creating this pull request, I understand that code that is AI
generated or otherwise automatically generated may be rejected without
further discussion.
I declare that I fully understand all code I pushed into this PR, and
wrote all this code myself and own the rights to this code.
2025-01-18 22:56:33 +01:00
Tixx
d52a791dd9 Create mods.json if its missing 2025-01-18 22:29:49 +01:00
Tixx
08a6f9a093 Log current and backend version (#169)
By creating this pull request, I understand that code that is AI
generated or otherwise automatically generated may be rejected without
further discussion.
I declare that I fully understand all code I pushed into this PR, and
wrote all this code myself and own the rights to this code.
2025-01-18 21:13:54 +01:00
Tixx
e5e40e186b Server info (#161)
Adds an `I` packet to the core handler, which allows the mod to use the
newly added [information packet
](https://github.com/BeamMP/BeamMP-Server/pull/382) in the server.
Usage:
Mod sends `I0.0.0.0:0000` and the launcher will send either
`I0.0.0.0:0000;` back if something went wrong, or `I0.0.0.0:0000;{Server
information JSON}` if it succeeded.

---

By creating this pull request, I understand that code that is AI
generated or otherwise automatically generated may be rejected without
further discussion.
I declare that I fully understand all code I pushed into this PR, and
wrote all this code myself and own the rights to this code.
2025-01-18 20:52:44 +01:00
Tixx
bfbff52cb1 Log current and backend version 2025-01-12 23:17:23 +01:00
Tixx
8d4ba6f158 Implement size header for info packet 2025-01-12 16:57:33 +01:00
Tixx
a5d450b680 Raise buffer and remove timeout 2025-01-11 22:13:12 +01:00
Tixx
f4e985976f Strip packet letter in log 2025-01-11 21:55:08 +01:00
Tixx
db9ec53a6e Up curl connection timeout to 2 minutes (#160)
In some cases it can take longer than 10 seconds to connect to the
backend, so in this PR I've raised the limit to 2 minutes.

---

By creating this pull request, I understand that code that is AI
generated or otherwise automatically generated may be rejected without
further discussion.
I declare that I fully understand all code I pushed into this PR, and
wrote all this code myself and own the rights to this code.
2025-01-11 21:54:35 +01:00
Tixx
f9b2edd410 Better curl debug (#165)
This PR makes it so the launcher logs curl's error description.

---

By creating this pull request, I understand that code that is AI
generated or otherwise automatically generated may be rejected without
further discussion.
I declare that I fully understand all code I pushed into this PR, and
wrote all this code myself and own the rights to this code.
2025-01-11 21:51:13 +01:00
Tixx
333a95262b Check port and timeout recv 2025-01-11 21:50:39 +01:00
Tixx
e53885a8a8 Raise http get timeout to 2 minutes 2025-01-11 21:45:06 +01:00
Tixx
c22ea1e85d Fix typo in README.md (#166)
Fix non-critical typo in README:md

---

By creating this pull request, I understand that code that is AI
generated or otherwise automatically generated may be rejected without
further discussion.
I declare that I fully understand all code I pushed into this PR, and
wrote all this code myself and own the rights to this code.
2025-01-08 16:41:24 +01:00
O1LER
f8ea9bd8a3 Fix typo in README.md 2025-01-08 16:38:29 +01:00
Tixx
ad8eab3d66 Log error buffer 2024-12-29 16:39:56 +01:00
Tixx
e880da5cf9 Up curl connection timeout to 2 minutes 2024-12-25 00:54:19 +01:00
Tixx
d14b64c652 Fix linux build 2024-12-25 00:07:13 +01:00
Tixx
649514ca1a Save mod usage date 2024-12-24 13:53:50 +01:00
Tixx
7149075d53 Implement core server information packet 2024-12-22 23:56:29 +01:00
Tixx
c485fba26b Change an easily confusable warning to debug 2024-12-22 12:13:59 +01:00
Tixx
d35567dd47 Look for new mods in the old format 2024-12-22 12:07:25 +01:00
21 changed files with 1208 additions and 538 deletions

View File

@@ -2,57 +2,7 @@
The launcher is the way we communitcate to outside the game, it does a few automated actions such as but not limited to: downloading the mod, launching the game, and create a connection to a server. The launcher is the way we communitcate to outside the game, it does a few automated actions such as but not limited to: downloading the mod, launching the game, and create a connection to a server.
**To clone this repository**: `git clone --recurse-submodules https://github.com/BeamMP/BeamMP-Launcher.git` ## [Getting started](https://docs.beammp.com/game/getting-started/)
## How to build for Windows
Make sure you have the necessary development tools installed:
[vcpkg](https://vcpkg.io/en/)
### Release
In the root directory of the project,
1. `cmake -DCMAKE_BUILD_TYPE=Release . -B bin -DCMAKE_TOOLCHAIN_FILE=C:/vcpkg/scripts/buildsystems/vcpkg.cmake -DVCPKG_TARGET_TRIPLET=x64-windows-static`
2. `cmake --build bin --parallel --config Release`
Remember to change `C:/vcpkg` to wherever you have vcpkg installed.
### Debug
In the root directory of the project,
1. `cmake . -B bin -DCMAKE_TOOLCHAIN_FILE=C:/vcpkg/scripts/buildsystems/vcpkg.cmake -DVCPKG_TARGET_TRIPLET=x64-windows-static`
2. `cmake --build bin --parallel`
Remember to change `C:/vcpkg` to wherever you have vcpkg installed.
## How to build for Linux
Make sure you have `vcpkg` installed, as well as basic development tools, often found in packages, for example:
- Debian: `sudo apt install build-essential`
- Fedora: `sudo dnf groupinstall "Development Tools"`
- Arch: `sudo pacman -S base-devel`
- openSUSE: `zypper in -t pattern devel-basis`
### Release
In the root directory of the project,
1. `cmake -DCMAKE_BUILD_TYPE=Release . -B bin -DCMAKE_TOOLCHAIN_FILE=~/vcpkg/scripts/buildsystems/vcpkg.cmake -DVCPKG_TARGET_TRIPLET=x64-linux`
2. `cmake --build bin --parallel --config Release`
### Debug
In the root directory of the project,
1. `cmake . -B bin -DCMAKE_TOOLCHAIN_FILE=~/vcpkg/scripts/buildsystems/vcpkg.cmake -DVCPKG_TARGET_TRIPLET=x64-linux`
2. `cmake --build bin --parallel`
## Running out of RAM while building
Should you run out of RAM while building, you can ommit the `--parallel` intruction, it will then use less RAM due to building only on one CPU thread.
You can also specify a number of threads to use, for example `--parallel 4` will use four CPU threads, but due to the small project size, you may be faster just omitting `--parallel` instead of trying to find the highest possible multithread number
## License ## License

View File

@@ -6,12 +6,14 @@
#pragma once #pragma once
#include "Logger.h" #include "Logger.h"
#include "Utils.h"
#include <string> #include <string>
class HTTP { class HTTP {
public: public:
static bool Download(const std::string& IP, const std::string& Fields, const std::string& Path); static bool Download(const std::string& IP, const beammp_fs_string& Path, const std::string& Hash);
static std::string Post(const std::string& IP, const std::string& Fields); static std::string Post(const std::string& IP, const std::string& Fields);
static std::string Get(const std::string& IP, const std::string& Fields = ""); static std::string Get(const std::string& IP);
static bool ProgressBar(size_t c, size_t t); static bool ProgressBar(size_t c, size_t t);
static void StartProxy(); static void StartProxy();
public: public:

View File

@@ -14,4 +14,11 @@ void debug(const std::string& toPrint);
void error(const std::string& toPrint); void error(const std::string& toPrint);
void info(const std::string& toPrint); void info(const std::string& toPrint);
void warn(const std::string& toPrint); void warn(const std::string& toPrint);
void except(const std::wstring& toPrint);
void fatal(const std::wstring& toPrint);
void debug(const std::wstring& toPrint);
void error(const std::wstring& toPrint);
void info(const std::wstring& toPrint);
void warn(const std::wstring& toPrint);
std::string getDate(); std::string getDate();

View File

@@ -6,6 +6,7 @@
#pragma once #pragma once
#include <filesystem>
#include <string> #include <string>
#ifdef __linux__ #ifdef __linux__
@@ -13,6 +14,7 @@
#include <bits/types/siginfo_t.h> #include <bits/types/siginfo_t.h>
#include <cstdint> #include <cstdint>
#include <sys/ucontext.h> #include <sys/ucontext.h>
#include <arpa/inet.h>
#endif #endif
void NetReset(); void NetReset();
@@ -28,13 +30,15 @@ extern bool Terminate;
extern uint64_t UDPSock; extern uint64_t UDPSock;
extern uint64_t TCPSock; extern uint64_t TCPSock;
extern std::string Branch; extern std::string Branch;
extern std::string CachingDirectory; extern std::filesystem::path CachingDirectory;
extern bool deleteDuplicateMods;
extern bool TCPTerminate; extern bool TCPTerminate;
extern std::string LastIP; extern std::string LastIP;
extern std::string MStatus; extern std::string MStatus;
extern std::string UlStatus; extern std::string UlStatus;
extern std::string PublicKey; extern std::string PublicKey;
extern std::string PrivateKey; extern std::string PrivateKey;
extern std::string magic;
int KillSocket(uint64_t Dead); int KillSocket(uint64_t Dead);
void UUl(const std::string& R); void UUl(const std::string& R);
void UDPSend(std::string Data); void UDPSend(std::string Data);

View File

@@ -19,6 +19,7 @@ struct Options {
bool no_download = false; bool no_download = false;
bool no_update = false; bool no_update = false;
bool no_launch = false; bool no_launch = false;
const char* user_path = nullptr;
const char **game_arguments = nullptr; const char **game_arguments = nullptr;
int game_arguments_length = 0; int game_arguments_length = 0;
const char** argv = nullptr; const char** argv = nullptr;

View File

@@ -6,9 +6,9 @@
#pragma once #pragma once
#include <string> #include <string>
void PreGame(const std::string& GamePath); void PreGame(const beammp_fs_string& GamePath);
std::string CheckVer(const std::string& path); std::string CheckVer(const std::filesystem::path& path);
void InitGame(const std::string& Dir); void InitGame(const beammp_fs_string& Dir);
std::string GetGameDir(); beammp_fs_string GetGameDir();
void LegitimacyCheck(); void LegitimacyCheck();
void CheckLocalKey(); void CheckLocalKey();

View File

@@ -5,14 +5,17 @@
*/ */
#pragma once #pragma once
#include "Utils.h"
#include <compare> #include <compare>
#include <string> #include <string>
#include <vector> #include <vector>
void InitLauncher(); void InitLauncher();
std::string GetEP(const char* P = nullptr); beammp_fs_string GetEP(const beammp_fs_char* P = nullptr);
std::string GetGamePath(); std::filesystem::path GetBP(const beammp_fs_char* P = nullptr);
std::filesystem::path GetGamePath();
std::string GetVer(); std::string GetVer();
std::string GetPatch(); std::string GetPatch();
std::string GetEN(); beammp_fs_string GetEN();
void ConfigInit(); void ConfigInit();

View File

@@ -5,8 +5,46 @@
*/ */
#pragma once #pragma once
#include <cassert>
#include <filesystem>
#include <fstream>
#include <locale>
#include <map>
#include <openssl/err.h>
#include <openssl/evp.h>
#include <regex>
#include <cstdint>
#include <cstring>
#include <string> #include <string>
#include <variant>
#include <vector> #include <vector>
#include <array>
#include <cerrno>
#include <cstring>
#include <stdexcept>
#if defined(__linux__)
#include <sys/socket.h>
#include "linuxfixes.h"
#else
#include <winsock2.h>
#include <ws2tcpip.h>
#endif
#ifdef _WIN32
#define beammp_fs_string std::wstring
#define beammp_fs_char wchar_t
#define beammp_wide(str) L##str
#define beammp_stdout std::wcout
#define WIN32_LEAN_AND_MEAN
#include <windows.h>
#else
#define beammp_fs_string std::string
#define beammp_fs_char char
#define beammp_wide(str) str
#define beammp_stdout std::cout
#endif
#include "Logger.h"
namespace Utils { namespace Utils {
inline std::vector<std::string> Split(const std::string& String, const std::string& delimiter) { inline std::vector<std::string> Split(const std::string& String, const std::string& delimiter) {
@@ -22,5 +60,280 @@ namespace Utils {
if (!s.empty()) if (!s.empty())
Val.push_back(s); Val.push_back(s);
return Val; return Val;
}; }
inline std::string ExpandEnvVars(const std::string& input) {
std::string result;
std::regex envPattern(R"(%([^%]+)%|\$([A-Za-z_][A-Za-z0-9_]*)|\$\{([^}]+)\})");
std::sregex_iterator begin(input.begin(), input.end(), envPattern);
std::sregex_iterator end;
size_t lastPos = 0;
for (auto it = begin; it != end; ++it) {
const auto& match = *it;
result.append(input, lastPos, match.position() - lastPos);
std::string varName;
if (match[1].matched) varName = match[1].str(); // %VAR%
else if (match[2].matched) varName = match[2].str(); // $VAR
else if (match[3].matched) varName = match[3].str(); // ${VAR}
if (const char* envValue = std::getenv(varName.c_str())) {
result.append(envValue);
}
lastPos = match.position() + match.length();
}
result.append(input, lastPos, input.length() - lastPos);
return result;
}
#ifdef _WIN32
inline std::wstring ExpandEnvVars(const std::wstring& input) {
std::wstring result;
std::wregex envPattern(LR"(%([^%]+)%|\$([A-Za-z_][A-Za-z0-9_]*)|\$\{([^}]+)\})");
std::wsregex_iterator begin(input.begin(), input.end(), envPattern);
std::wsregex_iterator end;
size_t lastPos = 0;
for (auto it = begin; it != end; ++it) {
const auto& match = *it;
result.append(input, lastPos, match.position() - lastPos);
std::wstring varName;
assert(match.size() == 4 && "Input regex has incorrect amount of capturing groups");
if (match[1].matched) varName = match[1].str(); // %VAR%
else if (match[2].matched) varName = match[2].str(); // $VAR
else if (match[3].matched) varName = match[3].str(); // ${VAR}
if (const wchar_t* envValue = _wgetenv(varName.c_str())) {
if (envValue != nullptr) {
result.append(envValue);
}
}
lastPos = match.position() + match.length();
}
result.append(input, lastPos, input.length() - lastPos);
return result;
}
#endif
inline std::map<std::string, std::variant<std::map<std::string, std::string>, std::string>> ParseINI(const std::string& contents) {
std::map<std::string, std::variant<std::map<std::string, std::string>, std::string>> ini;
std::string currentSection;
auto sections = Split(contents, "\n");
for (size_t i = 0; i < sections.size(); i++) {
std::string line = sections[i];
if (line.empty() || line[0] == ';' || line[0] == '#')
continue;
auto invalidLineLog = [&]{
debug("Invalid INI line: " + line);
debug("Surrounding lines: \n" +
(i > 0 ? sections[i - 1] : "") + "\n" +
(i < sections.size() - 1 ? sections[i + 1] : ""));
};
if (line[0] == '[') {
currentSection = line.substr(1, line.find(']') - 1);
} else {
std::string key, value;
size_t pos = line.find('=');
if (pos != std::string::npos) {
key = line.substr(0, pos);
key = key.substr(0, key.find_last_not_of(" \t") + 1);
value = line.substr(pos + 1);
if (currentSection.empty()) {
ini[key] = value;
} else {
std::get<std::map<std::string, std::string>>(ini[currentSection])[key] = value;
}
} else {
invalidLineLog();
continue;
}
key.erase(key.find_last_not_of(" \t") + 1);
value.erase(0, value.find_first_not_of(" \t"));
value.erase(value.find_last_not_of(" \t") + 1);
std::get<std::map<std::string, std::string>>(ini[currentSection])[key] = value;
}
}
return ini;
}
#ifdef _WIN32
inline std::wstring ToWString(const std::string& s) {
if (s.empty()) return std::wstring();
int size_needed = MultiByteToWideChar(CP_UTF8, 0, s.c_str(), (int)s.size(), nullptr, 0);
if (size_needed <= 0) {
return L"";
}
std::wstring result(size_needed, 0);
MultiByteToWideChar(CP_UTF8, 0, s.c_str(), (int)s.size(), &result[0], size_needed);
return result;
}
#else
inline std::string ToWString(const std::string& s) {
return s;
}
#endif
inline std::string GetSha256HashReallyFastFile(const beammp_fs_string& filename) {
try {
EVP_MD_CTX* mdctx;
const EVP_MD* md;
uint8_t sha256_value[EVP_MAX_MD_SIZE];
md = EVP_sha256();
if (md == nullptr) {
throw std::runtime_error("EVP_sha256() failed");
}
mdctx = EVP_MD_CTX_new();
if (mdctx == nullptr) {
throw std::runtime_error("EVP_MD_CTX_new() failed");
}
if (!EVP_DigestInit_ex2(mdctx, md, NULL)) {
EVP_MD_CTX_free(mdctx);
throw std::runtime_error("EVP_DigestInit_ex2() failed");
}
std::ifstream stream(filename, std::ios::binary);
const size_t FileSize = std::filesystem::file_size(filename);
size_t Read = 0;
std::vector<char> Data;
while (Read < FileSize) {
Data.resize(size_t(std::min<size_t>(FileSize - Read, 4096)));
size_t RealDataSize = Data.size();
stream.read(Data.data(), std::streamsize(Data.size()));
if (stream.eof() || stream.fail()) {
RealDataSize = size_t(stream.gcount());
}
Data.resize(RealDataSize);
if (RealDataSize == 0) {
break;
}
if (RealDataSize > 0 && !EVP_DigestUpdate(mdctx, Data.data(), Data.size())) {
EVP_MD_CTX_free(mdctx);
throw std::runtime_error("EVP_DigestUpdate() failed");
}
Read += RealDataSize;
}
unsigned int sha256_len = 0;
if (!EVP_DigestFinal_ex(mdctx, sha256_value, &sha256_len)) {
EVP_MD_CTX_free(mdctx);
throw std::runtime_error("EVP_DigestFinal_ex() failed");
}
EVP_MD_CTX_free(mdctx);
std::string result;
for (size_t i = 0; i < sha256_len; i++) {
char buf[3];
sprintf(buf, "%02x", sha256_value[i]);
buf[2] = '\0';
result += buf;
}
return result;
} catch (const std::exception& e) {
error(beammp_wide("Sha256 hashing of '") + filename + beammp_wide("' failed: ") + ToWString(e.what()));
return "";
}
}
inline std::string GetSha256HashReallyFast(const std::string& text, const beammp_fs_string& filename) {
try {
EVP_MD_CTX* mdctx;
const EVP_MD* md;
uint8_t sha256_value[EVP_MAX_MD_SIZE];
md = EVP_sha256();
if (md == nullptr) {
throw std::runtime_error("EVP_sha256() failed");
}
mdctx = EVP_MD_CTX_new();
if (mdctx == nullptr) {
throw std::runtime_error("EVP_MD_CTX_new() failed");
}
if (!EVP_DigestInit_ex2(mdctx, md, NULL)) {
EVP_MD_CTX_free(mdctx);
throw std::runtime_error("EVP_DigestInit_ex2() failed");
}
if (!EVP_DigestUpdate(mdctx, text.data(), text.size())) {
EVP_MD_CTX_free(mdctx);
throw std::runtime_error("EVP_DigestUpdate() failed");
}
unsigned int sha256_len = 0;
if (!EVP_DigestFinal_ex(mdctx, sha256_value, &sha256_len)) {
EVP_MD_CTX_free(mdctx);
throw std::runtime_error("EVP_DigestFinal_ex() failed");
}
EVP_MD_CTX_free(mdctx);
std::string result;
for (size_t i = 0; i < sha256_len; i++) {
char buf[3];
sprintf(buf, "%02x", sha256_value[i]);
buf[2] = '\0';
result += buf;
}
return result;
} catch (const std::exception& e) {
error(beammp_wide("Sha256 hashing of '") + filename + beammp_wide("' failed: ") + ToWString(e.what()));
return "";
}
}
template<typename T>
inline std::vector<char> PrependHeader(const T& data) {
std::vector<char> size_buffer(4);
uint32_t len = data.size();
std::memcpy(size_buffer.data(), &len, 4);
std::vector<char> buffer;
buffer.reserve(size_buffer.size() + data.size());
buffer.insert(buffer.begin(), size_buffer.begin(), size_buffer.end());
buffer.insert(buffer.end(), data.begin(), data.end());
return buffer;
}
inline uint32_t RecvHeader(SOCKET socket) {
std::array<uint8_t, sizeof(uint32_t)> header_buffer {};
auto n = recv(socket, reinterpret_cast<char*>(header_buffer.data()), header_buffer.size(), MSG_WAITALL);
if (n < 0) {
throw std::runtime_error(std::string("recv() of header failed: ") + std::strerror(errno));
} else if (n == 0) {
throw std::runtime_error("Game disconnected");
}
return *reinterpret_cast<uint32_t*>(header_buffer.data());
}
/// Throws!!!
inline void ReceiveFromGame(SOCKET socket, std::vector<char>& out_data) {
auto header = RecvHeader(socket);
out_data.resize(header);
auto n = recv(socket, reinterpret_cast<char*>(out_data.data()), out_data.size(), MSG_WAITALL);
if (n < 0) {
throw std::runtime_error(std::string("recv() of data failed: ") + std::strerror(errno));
} else if (n == 0) {
throw std::runtime_error("Game disconnected");
}
}
}; };

View File

@@ -6,15 +6,17 @@
#include "Logger.h" #include "Logger.h"
#include "Network/network.hpp" #include "Network/network.hpp"
#include "Options.h"
#include "Utils.h"
#include <cstdint> #include <cstdint>
#include <filesystem> #include <filesystem>
#include <fstream> #include <fstream>
#include <nlohmann/json.hpp> #include <nlohmann/json.hpp>
#include "Options.h"
namespace fs = std::filesystem; namespace fs = std::filesystem;
std::string Branch; std::string Branch;
std::string CachingDirectory = "./Resources"; std::filesystem::path CachingDirectory = std::filesystem::path("./Resources");
bool deleteDuplicateMods = false;
void ParseConfig(const nlohmann::json& d) { void ParseConfig(const nlohmann::json& d) {
if (d["Port"].is_number()) { if (d["Port"].is_number()) {
@@ -31,8 +33,8 @@ void ParseConfig(const nlohmann::json& d) {
c = char(tolower(c)); c = char(tolower(c));
} }
if (d.contains("CachingDirectory") && d["CachingDirectory"].is_string()) { if (d.contains("CachingDirectory") && d["CachingDirectory"].is_string()) {
CachingDirectory = d["CachingDirectory"].get<std::string>(); CachingDirectory = std::filesystem::path(d["CachingDirectory"].get<std::string>());
info("Mod caching directory: " + CachingDirectory); info(beammp_wide("Mod caching directory: ") + beammp_fs_string(CachingDirectory.relative_path()));
} }
if (d.contains("Dev") && d["Dev"].is_boolean()) { if (d.contains("Dev") && d["Dev"].is_boolean()) {
@@ -42,6 +44,11 @@ void ParseConfig(const nlohmann::json& d) {
options.no_launch = dev; options.no_launch = dev;
options.no_update = dev; options.no_update = dev;
} }
if (d.contains(("DeleteDuplicateMods")) && d["DeleteDuplicateMods"].is_boolean()) {
deleteDuplicateMods = d["DeleteDuplicateMods"].get<bool>();
}
} }
void ConfigInit() { void ConfigInit() {

View File

@@ -4,9 +4,9 @@
SPDX-License-Identifier: AGPL-3.0-or-later SPDX-License-Identifier: AGPL-3.0-or-later
*/ */
#include <cstring>
#include "Utils.h"
#if defined(_WIN32) #if defined(_WIN32)
#include <windows.h>
#include <shlobj.h> #include <shlobj.h>
#elif defined(__linux__) #elif defined(__linux__)
#include "vdf_parser.hpp" #include "vdf_parser.hpp"
@@ -18,93 +18,162 @@
#endif #endif
#include "Logger.h" #include "Logger.h"
#include "Options.h"
#include "Startup.h" #include "Startup.h"
#include <Security/Init.h> #include <Security/Init.h>
#include <filesystem> #include <filesystem>
#include <thread> #include <thread>
#include "Options.h"
#include <fstream>
unsigned long GamePID = 0; unsigned long GamePID = 0;
#if defined(_WIN32) #if defined(_WIN32)
std::string QueryKey(HKEY hKey, int ID); std::wstring QueryKey(HKEY hKey, int ID);
std::string GetGamePath() { std::filesystem::path GetGamePath() {
static std::string Path; static std::filesystem::path Path;
if (!Path.empty()) if (!Path.empty())
return Path; return Path.wstring();
HKEY hKey; if (options.user_path) {
LPCTSTR sk = "Software\\BeamNG\\BeamNG.drive"; if (std::filesystem::exists(options.user_path)) {
LONG openRes = RegOpenKeyEx(HKEY_CURRENT_USER, sk, 0, KEY_ALL_ACCESS, &hKey); Path = options.user_path;
if (openRes != ERROR_SUCCESS) { debug(L"Using custom user folder path: " + Path.wstring());
fatal("Please launch the game at least once!"); } else
warn(L"Invalid or non-existent path (" + Utils::ToWString(options.user_path) + L") specified using --user-path, skipping");
} }
Path = QueryKey(hKey, 4);
if (Path.empty()) { if (const auto startupIniPath = std::filesystem::path(GetGameDir()) / "startup.ini"; exists(startupIniPath)) {
Path = "";
char appDataPath[MAX_PATH]; if (std::ifstream startupIni(startupIniPath); startupIni.is_open()) {
HRESULT result = SHGetFolderPathA(NULL, CSIDL_LOCAL_APPDATA, NULL, 0, appDataPath); std::string contents((std::istreambuf_iterator(startupIni)), std::istreambuf_iterator<char>());
if (SUCCEEDED(result)) { startupIni.close();
Path = appDataPath;
auto ini = Utils::ParseINI(contents);
if (ini.empty())
warn("Failed to parse startup.ini");
else
debug("Successfully parsed startup.ini");
std::wstring userPath;
if (ini.contains("filesystem") && std::get<std::map<std::string, std::string>>(ini["filesystem"]).contains("UserPath"))
userPath = Utils::ToWString(std::get<std::map<std::string, std::string>>(ini["filesystem"])["UserPath"]);
if (userPath = Utils::ExpandEnvVars(userPath); std::filesystem::exists(userPath)) {
Path = userPath;
debug(L"Using custom user folder path from startup.ini: " + Path.wstring());
} else
warn(L"Found custom user folder path (" + userPath + L") in startup.ini but it doesn't exist, skipping");
} }
if (Path.empty()) { if (Path.empty()) {
fatal("Cannot get Local Appdata directory"); wchar_t* appDataPath = new wchar_t[MAX_PATH];
} HRESULT result = SHGetFolderPathW(NULL, CSIDL_LOCAL_APPDATA, NULL, 0, appDataPath);
Path += "\\BeamNG.drive\\"; if (!SUCCEEDED(result)) {
fatal("Cannot get Local Appdata directory");
}
auto BeamNGAppdataPath = std::filesystem::path(appDataPath) / "BeamNG";
if (const auto beamngIniPath = BeamNGAppdataPath / "BeamNG.Drive.ini"; exists(beamngIniPath)) {
if (std::ifstream beamngIni(beamngIniPath); beamngIni.is_open()) {
std::string contents((std::istreambuf_iterator(beamngIni)), std::istreambuf_iterator<char>());
beamngIni.close();
auto ini = Utils::ParseINI(contents);
if (ini.empty())
warn("Failed to parse BeamNG.Drive.ini");
else
debug("Successfully parsed BeamNG.Drive.ini");
std::wstring userPath;
if (ini.contains("userFolder")) {
userPath = Utils::ToWString(std::get<std::string>(ini["userFolder"]));
userPath.erase(0, userPath.find_first_not_of(L" \t"));
}
if (userPath = std::filesystem::path(Utils::ExpandEnvVars(userPath)); std::filesystem::exists(userPath)) {
Path = userPath;
debug(L"Using custom user folder path from BeamNG.Drive.ini: " + Path.wstring());
} else
warn(L"Found custom user folder path (" + userPath + L") in BeamNG.Drive.ini but it doesn't exist, skipping");
}
}
if (Path.empty()) {
Path = BeamNGAppdataPath / "BeamNG.drive";
}
delete[] appDataPath;
}
} }
std::string Ver = CheckVer(GetGameDir()); std::string Ver = CheckVer(GetGameDir());
Ver = Ver.substr(0, Ver.find('.', Ver.find('.') + 1)); Ver = Ver.substr(0, Ver.find('.', Ver.find('.') + 1));
Path += Ver + "\\"; Path /= Utils::ToWString("current");
return Path; return Path;
} }
#elif defined(__linux__) #elif defined(__linux__)
std::string GetGamePath() { std::filesystem::path GetGamePath() {
// Right now only steam is supported // Right now only steam is supported
struct passwd* pw = getpwuid(getuid()); struct passwd* pw = getpwuid(getuid());
std::string homeDir = pw->pw_dir; std::string homeDir = pw->pw_dir;
std::string Path = homeDir + "/.local/share/BeamNG.drive/"; std::string Path = homeDir + "/.local/share/BeamNG/BeamNG.drive/";
std::string Ver = CheckVer(GetGameDir()); std::string Ver = CheckVer(GetGameDir());
Ver = Ver.substr(0, Ver.find('.', Ver.find('.') + 1)); Ver = Ver.substr(0, Ver.find('.', Ver.find('.') + 1));
Path += Ver + "/"; Path += "current/";
return Path; return Path;
} }
#endif #endif
#if defined(_WIN32) #if defined(_WIN32)
void StartGame(std::string Dir) { void StartGame(std::wstring Dir) {
BOOL bSuccess = FALSE; BOOL bSuccess = FALSE;
PROCESS_INFORMATION pi; PROCESS_INFORMATION pi;
STARTUPINFO si = { 0 }; STARTUPINFOW si = { 0 };
si.cb = sizeof(si); si.cb = sizeof(si);
std::string BaseDir = Dir; //+"\\Bin64"; std::wstring BaseDir = Dir; //+"\\Bin64";
// Dir += R"(\Bin64\BeamNG.drive.x64.exe)"; // Dir += R"(\Bin64\BeamNG.drive.x64.exe)";
Dir += "\\BeamNG.drive.exe"; Dir += L"\\BeamNG.drive.exe";
std::string gameArgs = ""; std::wstring gameArgs = L"";
for (int i = 0; i < options.game_arguments_length; i++) { for (int i = 0; i < options.game_arguments_length; i++) {
gameArgs += " "; gameArgs += L" ";
gameArgs += options.game_arguments[i]; gameArgs += Utils::ToWString(options.game_arguments[i]);
} }
bSuccess = CreateProcessA(nullptr, (LPSTR)(Dir + gameArgs).c_str(), nullptr, nullptr, TRUE, 0, nullptr, BaseDir.c_str(), &si, &pi); debug(L"BeamNG executable path: " + Dir);
bSuccess = CreateProcessW(nullptr, (wchar_t*)(Dir + gameArgs).c_str(), nullptr, nullptr, TRUE, 0, nullptr, BaseDir.c_str(), &si, &pi);
if (bSuccess) { if (bSuccess) {
info("Game Launched!"); info("Game Launched!");
GamePID = pi.dwProcessId; GamePID = pi.dwProcessId;
WaitForSingleObject(pi.hProcess, INFINITE); WaitForSingleObject(pi.hProcess, INFINITE);
error("Game Closed! launcher closing soon"); error("Game Closed! launcher closing soon");
} else { } else {
error("Failed to Launch the game! launcher closing soon"); std::string err = "";
DWORD dw = GetLastError();
LPVOID lpErrorMsgBuffer;
if (FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS, NULL, dw,
MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPTSTR)&lpErrorMsgBuffer, 0, nullptr)
== 0) {
err = "Unknown error code: " + std::to_string(dw);
} else {
err = "Error " + std::to_string(dw) + ": " + (char*)lpErrorMsgBuffer;
}
error("Failed to Launch the game! launcher closing soon. " + err);
} }
std::this_thread::sleep_for(std::chrono::seconds(5)); std::this_thread::sleep_for(std::chrono::seconds(5));
exit(2); exit(2);
} }
#elif defined(__linux__) #elif defined(__linux__)
void StartGame(std::string Dir) { void StartGame(std::string Dir) {
int status;
std::string filename = (Dir + "/BinLinux/BeamNG.drive.x64"); std::string filename = (Dir + "/BinLinux/BeamNG.drive.x64");
std::vector<const char*> argv; std::vector<const char*> argv;
argv.push_back(filename.data()); argv.push_back(filename.data());
@@ -114,11 +183,24 @@ void StartGame(std::string Dir) {
argv.push_back(nullptr); argv.push_back(nullptr);
pid_t pid; pid_t pid;
posix_spawn_file_actions_t spawn_actions;
posix_spawn_file_actions_init(&spawn_actions); posix_spawn_file_actions_t file_actions;
posix_spawn_file_actions_addclose(&spawn_actions, STDOUT_FILENO); auto status = posix_spawn_file_actions_init(&file_actions);
posix_spawn_file_actions_addclose(&spawn_actions, STDERR_FILENO); // disable stdout
int result = posix_spawn(&pid, filename.c_str(), &spawn_actions, nullptr, const_cast<char**>(argv.data()), environ); if (status != 0) {
error(std::string("posix_spawn_file_actions_init failed: ") + std::strerror(errno));
}
status = posix_spawn_file_actions_addclose(&file_actions, STDOUT_FILENO);
if (status != 0) {
error(std::string("posix_spawn_file_actions_addclose for STDOUT failed: ") + std::strerror(errno));
}
status = posix_spawn_file_actions_addclose(&file_actions, STDERR_FILENO);
if (status != 0) {
error(std::string("posix_spawn_file_actions_addclose for STDERR failed: ") + std::strerror(errno));
}
// launch the game
int result = posix_spawn(&pid, filename.c_str(), &file_actions, NULL, const_cast<char**>(argv.data()), environ);
if (result != 0) { if (result != 0) {
error("Failed to Launch the game! launcher closing soon"); error("Failed to Launch the game! launcher closing soon");
@@ -133,7 +215,7 @@ void StartGame(std::string Dir) {
} }
#endif #endif
void InitGame(const std::string& Dir) { void InitGame(const beammp_fs_string& Dir) {
if (!options.no_launch) { if (!options.no_launch) {
std::thread Game(StartGame, Dir); std::thread Game(StartGame, Dir);
Game.detach(); Game.detach();

View File

@@ -7,6 +7,7 @@
#include "Logger.h" #include "Logger.h"
#include "Startup.h" #include "Startup.h"
#include "Utils.h"
#include <chrono> #include <chrono>
#include <fstream> #include <fstream>
#include <sstream> #include <sstream>
@@ -36,7 +37,7 @@ std::string getDate() {
} }
void InitLog() { void InitLog() {
std::ofstream LFS; std::ofstream LFS;
LFS.open(GetEP() + "Launcher.log"); LFS.open(GetEP() + beammp_wide("Launcher.log"));
if (!LFS.is_open()) { if (!LFS.is_open()) {
error("logger file init failed!"); error("logger file init failed!");
} else } else
@@ -44,41 +45,85 @@ void InitLog() {
} }
void addToLog(const std::string& Line) { void addToLog(const std::string& Line) {
std::ofstream LFS; std::ofstream LFS;
LFS.open(GetEP() + "Launcher.log", std::ios_base::app); LFS.open(GetEP() + beammp_wide("Launcher.log"), std::ios_base::app);
LFS << Line.c_str();
LFS.close();
}
void addToLog(const std::wstring& Line) {
std::wofstream LFS;
LFS.open(GetEP() + beammp_wide("Launcher.log"), std::ios_base::app);
LFS << Line.c_str(); LFS << Line.c_str();
LFS.close(); LFS.close();
} }
void info(const std::string& toPrint) { void info(const std::string& toPrint) {
std::string Print = getDate() + "[INFO] " + toPrint + "\n"; std::string Print = getDate() + "[INFO] " + toPrint + "\n";
std::cout << Print; beammp_stdout << Utils::ToWString(Print);
addToLog(Print); addToLog(Print);
} }
void debug(const std::string& toPrint) { void debug(const std::string& toPrint) {
std::string Print = getDate() + "[DEBUG] " + toPrint + "\n"; std::string Print = getDate() + "[DEBUG] " + toPrint + "\n";
if (options.verbose) { if (options.verbose) {
std::cout << Print; beammp_stdout << Utils::ToWString(Print);
} }
addToLog(Print); addToLog(Print);
} }
void warn(const std::string& toPrint) { void warn(const std::string& toPrint) {
std::string Print = getDate() + "[WARN] " + toPrint + "\n"; std::string Print = getDate() + "[WARN] " + toPrint + "\n";
std::cout << Print; beammp_stdout << Utils::ToWString(Print);
addToLog(Print); addToLog(Print);
} }
void error(const std::string& toPrint) { void error(const std::string& toPrint) {
std::string Print = getDate() + "[ERROR] " + toPrint + "\n"; std::string Print = getDate() + "[ERROR] " + toPrint + "\n";
std::cout << Print; beammp_stdout << Utils::ToWString(Print);
addToLog(Print); addToLog(Print);
} }
void fatal(const std::string& toPrint) { void fatal(const std::string& toPrint) {
std::string Print = getDate() + "[FATAL] " + toPrint + "\n"; std::string Print = getDate() + "[FATAL] " + toPrint + "\n";
std::cout << Print; beammp_stdout << Utils::ToWString(Print);
addToLog(Print); addToLog(Print);
std::this_thread::sleep_for(std::chrono::seconds(5)); std::this_thread::sleep_for(std::chrono::seconds(5));
std::exit(1); std::exit(1);
} }
void except(const std::string& toPrint) { void except(const std::string& toPrint) {
std::string Print = getDate() + "[EXCEP] " + toPrint + "\n"; std::string Print = getDate() + "[EXCEP] " + toPrint + "\n";
std::cout << Print; beammp_stdout << Utils::ToWString(Print);
addToLog(Print); addToLog(Print);
} }
#ifdef _WIN32
void info(const std::wstring& toPrint) {
std::wstring Print = Utils::ToWString(getDate()) + L"[INFO] " + toPrint + L"\n";
std::wcout << Print;
addToLog(Print);
}
void debug(const std::wstring& toPrint) {
std::wstring Print = Utils::ToWString(getDate()) + L"[DEBUG] " + toPrint + L"\n";
if (options.verbose) {
std::wcout << Print;
}
addToLog(Print);
}
void warn(const std::wstring& toPrint) {
std::wstring Print = Utils::ToWString(getDate()) + L"[WARN] " + toPrint + L"\n";
std::wcout << Print;
addToLog(Print);
}
void error(const std::wstring& toPrint) {
std::wstring Print = Utils::ToWString(getDate()) + L"[ERROR] " + toPrint + L"\n";
std::wcout << Print;
addToLog(Print);
}
void fatal(const std::wstring& toPrint) {
std::wstring Print = Utils::ToWString(getDate()) + L"[FATAL] " + toPrint + L"\n";
std::wcout << Print;
addToLog(Print);
std::this_thread::sleep_for(std::chrono::seconds(5));
std::exit(1);
}
void except(const std::wstring& toPrint) {
std::wstring Print = Utils::ToWString(getDate()) + L"[EXCEP] " + toPrint + L"\n";
std::wcout << Print;
addToLog(Print);
}
#endif

View File

@@ -7,11 +7,13 @@
#include "Http.h" #include "Http.h"
#include "Network/network.hpp" #include "Network/network.hpp"
#include "Security/Init.h" #include "Security/Init.h"
#include "Utils.h"
#include <cstdlib> #include <cstdlib>
#include <regex> #include <regex>
#if defined(_WIN32) #if defined(_WIN32)
#include <winsock2.h> #include <winsock2.h>
#include <ws2tcpip.h> #include <ws2tcpip.h>
#include <shellapi.h>
#elif defined(__linux__) #elif defined(__linux__)
#include <cstring> #include <cstring>
#include <errno.h> #include <errno.h>
@@ -89,13 +91,106 @@ void StartSync(const std::string& Data) {
info("Connecting to server"); info("Connecting to server");
} }
void GetServerInfo(std::string Data) {
debug("Fetching server info of " + Data.substr(1));
std::string IP = GetAddr(Data.substr(1, Data.find(':') - 1));
if (IP.find('.') == -1) {
if (IP == "DNS")
warn("Connection Failed! (DNS Lookup Failed) for " + Data);
else
warn("Connection Failed! (WSA failed to start) for " + Data);
CoreSend("I" + Data + ";");
return;
}
SOCKET ISock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
SOCKADDR_IN ServerAddr;
if (ISock < 1) {
debug("Socket creation failed with error: " + std::to_string(WSAGetLastError()));
KillSocket(ISock);
CoreSend("I" + Data + ";");
return;
}
ServerAddr.sin_family = AF_INET;
int port = std::stoi(Data.substr(Data.find(':') + 1));
if (port < 1 || port > 65535) {
debug("Invalid port number: " + std::to_string(port));
KillSocket(ISock);
CoreSend("I" + Data + ";");
return;
}
ServerAddr.sin_port = htons(port);
inet_pton(AF_INET, IP.c_str(), &ServerAddr.sin_addr);
if (connect(ISock, (SOCKADDR*)&ServerAddr, sizeof(ServerAddr)) != 0) {
debug("Connection to server failed with error: " + std::to_string(WSAGetLastError()));
KillSocket(ISock);
CoreSend("I" + Data + ";");
return;
}
char Code[1] = { 'I' };
if (send(ISock, Code, 1, 0) != 1) {
debug("Sending data to server failed with error: " + std::to_string(WSAGetLastError()));
KillSocket(ISock);
CoreSend("I" + Data + ";");
return;
}
const std::string buffer = ([&]() -> std::string {
int32_t Header;
std::vector<char> data(sizeof(Header));
int Temp = recv(ISock, data.data(), sizeof(Header), MSG_WAITALL);
auto checkBytes = ([&](const int32_t bytes) -> bool {
if (bytes == 0) {
return false;
} else if (bytes < 0) {
return false;
}
return true;
});
if (!checkBytes(Temp)) {
return "";
}
memcpy(&Header, data.data(), sizeof(Header));
if (!checkBytes(Temp)) {
return "";
}
data.resize(Header, 0);
Temp = recv(ISock, data.data(), Header, MSG_WAITALL);
if (!checkBytes(Temp)) {
return "";
}
return std::string(data.data(), Header);
})();
if (!buffer.empty()) {
debug("Server Info: " + buffer);
CoreSend("I" + Data + ";" + buffer);
} else {
debug("Receiving data from server failed with error: " + std::to_string(WSAGetLastError()));
debug("Failed to receive server info from " + Data);
CoreSend("I" + Data + ";");
}
KillSocket(ISock);
}
std::mutex sendMutex; std::mutex sendMutex;
void CoreSend(std::string data) { void CoreSend(std::string data) {
std::lock_guard lock(sendMutex); std::lock_guard lock(sendMutex);
if (CoreSocket != -1) { if (CoreSocket != -1) {
int res = send(CoreSocket, (data + "\n").c_str(), int(data.size()) + 1, 0); auto ToSend = Utils::PrependHeader(data);
int res = send(CoreSocket, ToSend.data(), ToSend.size(), 0);
if (res < 0) { if (res < 0) {
debug("(Core) send failed with error: " + std::to_string(WSAGetLastError())); debug("(Core) send failed with error: " + std::to_string(WSAGetLastError()));
} }
@@ -108,7 +203,17 @@ bool IsAllowedLink(const std::string& Link) {
return std::regex_search(Link, link_match, link_pattern) && link_match.position() == 0; return std::regex_search(Link, link_match, link_pattern) && link_match.position() == 0;
} }
std::vector<std::future<void>> futures;
void Parse(std::string Data, SOCKET CSocket) { void Parse(std::string Data, SOCKET CSocket) {
std::erase_if(futures, [](const std::future<void>& f) {
if (f.wait_for(std::chrono::seconds(0)) == std::future_status::ready) {
return true;
}
return false;
});
char Code = Data.at(0), SubCode = 0; char Code = Data.at(0), SubCode = 0;
if (Data.length() > 1) if (Data.length() > 1)
SubCode = Data.at(1); SubCode = Data.at(1);
@@ -121,9 +226,9 @@ void Parse(std::string Data, SOCKET CSocket) {
Terminate = true; Terminate = true;
TCPTerminate = true; TCPTerminate = true;
Data.clear(); Data.clear();
auto future = std::async(std::launch::async, []() { futures.push_back(std::async(std::launch::async, []() {
CoreSend("B" + HTTP::Get("https://backend.beammp.com/servers-info")); // CoreSend("B" + HTTP::Get("https://backend.beammp.com/servers-info"));
}); }));
} }
break; break;
case 'C': case 'C':
@@ -175,7 +280,7 @@ void Parse(std::string Data, SOCKET CSocket) {
Ping = "-2"; Ping = "-2";
else else
Ping = std::to_string(ping); Ping = std::to_string(ping);
Data = std::string(UlStatus) + "\n" + "Up" + Ping; Data = "Up" + Ping;
} }
break; break;
case 'M': case 'M':
@@ -220,9 +325,9 @@ void Parse(std::string Data, SOCKET CSocket) {
} }
Data = "N" + Auth.dump(); Data = "N" + Auth.dump();
} else { } else {
auto future = std::async(std::launch::async, [data = std::move(Data)]() { futures.push_back(std::async(std::launch::async, [data = std::move(Data)]() {
CoreSend("N" + Login(data.substr(data.find(':') + 1))); CoreSend("N" + Login(data.substr(data.find(':') + 1)));
}); }));
Data.clear(); Data.clear();
} }
break; break;
@@ -235,54 +340,39 @@ void Parse(std::string Data, SOCKET CSocket) {
Data.clear(); Data.clear();
break; break;
case 'I': {
auto future = std::async(std::launch::async, [data = std::move(Data)]() {
GetServerInfo(data);
});
break;
}
default: default:
Data.clear(); Data.clear();
break; break;
} }
if (!Data.empty()) if (!Data.empty() && CSocket != -1) {
CoreSend(Data); auto ToSend = Utils::PrependHeader(Data);
int res = send(CSocket, ToSend.data(), ToSend.size(), 0);
if (res < 0) {
debug("(Core) send failed with error: " + std::to_string(WSAGetLastError()));
}
}
} }
void GameHandler(SOCKET Client) { void GameHandler(SOCKET Client) {
CoreSocket = Client; CoreSocket = Client;
int32_t Size, Temp, Rcv; std::vector<char> data{};
int Temp;
char Header[10] = { 0 }; char Header[10] = { 0 };
do { do {
Rcv = 0; try {
do { Utils::ReceiveFromGame(Client, data);
Temp = recv(Client, &Header[Rcv], 1, 0); Parse(std::string(data.data(), data.size()), Client);
if (Temp < 1) } catch (const std::exception& e) {
break; error(std::string("Error while receiving from game on core: ") + e.what());
if (!isdigit(Header[Rcv]) && Header[Rcv] != '>') {
error("(Core) Invalid lua communication");
KillSocket(Client);
return;
}
} while (Header[Rcv++] != '>');
if (Temp < 1)
break;
if (std::from_chars(Header, &Header[Rcv], Size).ptr[0] != '>') {
debug("(Core) Invalid lua Header -> " + std::string(Header, Rcv));
break; break;
} }
std::string Ret(Size, 0); } while (true);
Rcv = 0; debug("(Core) Connection closing");
do {
Temp = recv(Client, &Ret[Rcv], Size - Rcv, 0);
if (Temp < 1)
break;
Rcv += Temp;
} while (Rcv < Size);
if (Temp < 1)
break;
Parse(Ret, Client);
} while (Temp > 0);
if (Temp == 0) {
debug("(Core) Connection closing");
} else {
debug("(Core) recv failed with error: " + std::to_string(WSAGetLastError()));
}
NetReset(); NetReset();
KillSocket(Client); KillSocket(Client);
} }

View File

@@ -5,6 +5,7 @@
*/ */
#include "Network/network.hpp" #include "Network/network.hpp"
#include "Utils.h"
#include <memory> #include <memory>
#include <zlib.h> #include <zlib.h>
#if defined(_WIN32) #if defined(_WIN32)
@@ -21,17 +22,20 @@
#endif #endif
#include "Logger.h" #include "Logger.h"
#include "Options.h"
#include <charconv> #include <charconv>
#include <mutex> #include <mutex>
#include <string> #include <string>
#include <thread> #include <thread>
#include "Options.h" #include "Options.h"
#include <chrono>
std::chrono::time_point<std::chrono::high_resolution_clock> PingStart, PingEnd; std::chrono::time_point<std::chrono::high_resolution_clock> PingStart, PingEnd;
bool GConnected = false; bool GConnected = false;
bool CServer = true; bool CServer = true;
SOCKET CSocket = -1; SOCKET CSocket = -1;
SOCKET GSocket = -1; SOCKET GSocket = -1;
std::string magic;
int KillSocket(uint64_t Dead) { int KillSocket(uint64_t Dead) {
if (Dead == (SOCKET)-1) { if (Dead == (SOCKET)-1) {
@@ -60,30 +64,13 @@ bool CheckBytes(uint32_t Bytes) {
void GameSend(std::string_view Data) { void GameSend(std::string_view Data) {
static std::mutex Lock; static std::mutex Lock;
std::scoped_lock Guard(Lock); std::scoped_lock Guard(Lock);
if (TCPTerminate || !GConnected || CSocket == -1) auto ToSend = Utils::PrependHeader<std::string_view>(Data);
return; auto Result = send(CSocket, ToSend.data(), ToSend.size(), 0);
int32_t Size, Temp, Sent; if (Result < 0) {
Size = int32_t(Data.size()); error("(Game) send failed with error: " + std::to_string(WSAGetLastError()));
Sent = 0;
#ifdef DEBUG
if (Size > 1000) {
debug("Launcher -> game (" + std::to_string(Size) + ")");
}
#endif
do {
if (Sent > -1) {
Temp = send(CSocket, &Data[Sent], Size - Sent, 0);
}
if (!CheckBytes(Temp))
return;
Sent += Temp;
} while (Sent < Size);
// send separately to avoid an allocation for += "\n"
Temp = send(CSocket, "\n", 1, 0);
if (!CheckBytes(Temp)) {
return;
} }
} }
void ServerSend(std::string Data, bool Rel) { void ServerSend(std::string Data, bool Rel) {
if (Terminate || Data.empty()) if (Terminate || Data.empty())
return; return;
@@ -217,6 +204,8 @@ void ParserAsync(std::string_view Data) {
MStatus = Data; MStatus = Data;
UlStatus = "Uldone"; UlStatus = "Uldone";
return; return;
case 'U':
magic = Data.substr(1);
default: default:
break; break;
} }
@@ -261,42 +250,22 @@ void TCPGameServer(const std::string& IP, int Port) {
NetMainThread = std::make_unique<std::thread>(NetMain, IP, Port); NetMainThread = std::make_unique<std::thread>(NetMain, IP, Port);
CServer = false; CServer = false;
} }
int32_t Size, Temp, Rcv; int32_t Size, Rcv;
int Temp;
char Header[10] = { 0 }; char Header[10] = { 0 };
std::vector<char> data {};
// Read byte by byte until '>' is rcved then get the size and read based on it // Read byte by byte until '>' is rcved then get the size and read based on it
do { do {
Rcv = 0; try {
Utils::ReceiveFromGame(CSocket, data);
do { ServerSend(std::string(data.data(), data.size()), false);
Temp = recv(CSocket, &Header[Rcv], 1, 0); } catch (const std::exception& e) {
if (Temp < 1 || TCPTerminate) error(std::string("Error while receiving from game on proxy: ") + e.what());
break;
} while (Header[Rcv++] != '>');
if (Temp < 1 || TCPTerminate)
break;
if (std::from_chars(Header, &Header[Rcv], Size).ptr[0] != '>') {
debug("(Game) Invalid lua Header -> " + std::string(Header, Rcv));
break; break;
} }
std::string Ret(Size, 0); } while (!TCPTerminate);
Rcv = 0; debug("(Proxy) Connection closing");
do {
Temp = recv(CSocket, &Ret[Rcv], Size - Rcv, 0);
if (Temp < 1)
break;
Rcv += Temp;
} while (Rcv < Size && !TCPTerminate);
if (Temp < 1 || TCPTerminate)
break;
ServerSend(Ret, false);
} while (Temp > 0 && !TCPTerminate);
if (Temp == 0)
debug("(Proxy) Connection closing");
else
debug("(Proxy) recv failed error : " + std::to_string(WSAGetLastError()));
} }
TCPTerminate = true; TCPTerminate = true;
GConnected = false; GConnected = false;

View File

@@ -68,24 +68,23 @@ static size_t CurlWriteCallback(void* contents, size_t size, size_t nmemb, void*
} }
bool HTTP::isDownload = false; bool HTTP::isDownload = false;
std::string HTTP::Get(const std::string& IP, const std::string& Fields) { std::string HTTP::Get(const std::string& IP) {
std::string Ret; std::string Ret;
static thread_local CURL* curl = curl_easy_init(); static thread_local CURL* curl = curl_easy_init();
if (curl) { if (curl) {
CURLcode res; CURLcode res;
char errbuf[CURL_ERROR_SIZE];
curl_easy_setopt(curl, CURLOPT_URL, IP.c_str()); curl_easy_setopt(curl, CURLOPT_URL, IP.c_str());
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, CurlWriteCallback); curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, CurlWriteCallback);
curl_easy_setopt(curl, CURLOPT_WRITEDATA, (void*)&Ret); curl_easy_setopt(curl, CURLOPT_WRITEDATA, (void*)&Ret);
curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT, 10); // seconds curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT, 120); // seconds
curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L); curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L);
if (!Fields.empty()) { curl_easy_setopt(curl, CURLOPT_ERRORBUFFER, errbuf);
curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, "GET"); errbuf[0] = 0;
curl_easy_setopt(curl, CURLOPT_POSTFIELDS, Fields.c_str());
curl_easy_setopt(curl, CURLOPT_POSTFIELDSIZE, Fields.size());
}
res = curl_easy_perform(curl); res = curl_easy_perform(curl);
if (res != CURLE_OK) { if (res != CURLE_OK) {
error("GET to " + IP + " failed: " + std::string(curl_easy_strerror(res))); error("GET to " + IP + " failed: " + std::string(curl_easy_strerror(res)));
error("Curl error: " + std::string(errbuf));
return ""; return "";
} }
} else { } else {
@@ -100,6 +99,7 @@ std::string HTTP::Post(const std::string& IP, const std::string& Fields) {
static thread_local CURL* curl = curl_easy_init(); static thread_local CURL* curl = curl_easy_init();
if (curl) { if (curl) {
CURLcode res; CURLcode res;
char errbuf[CURL_ERROR_SIZE];
curl_easy_setopt(curl, CURLOPT_URL, IP.c_str()); curl_easy_setopt(curl, CURLOPT_URL, IP.c_str());
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, CurlWriteCallback); curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, CurlWriteCallback);
curl_easy_setopt(curl, CURLOPT_WRITEDATA, (void*)&Ret); curl_easy_setopt(curl, CURLOPT_WRITEDATA, (void*)&Ret);
@@ -109,12 +109,15 @@ std::string HTTP::Post(const std::string& IP, const std::string& Fields) {
struct curl_slist* list = nullptr; struct curl_slist* list = nullptr;
list = curl_slist_append(list, "Content-Type: application/json"); list = curl_slist_append(list, "Content-Type: application/json");
curl_easy_setopt(curl, CURLOPT_HTTPHEADER, list); curl_easy_setopt(curl, CURLOPT_HTTPHEADER, list);
curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT, 10); // seconds curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT, 120); // seconds
curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L); curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L);
curl_easy_setopt(curl, CURLOPT_ERRORBUFFER, errbuf);
errbuf[0] = 0;
res = curl_easy_perform(curl); res = curl_easy_perform(curl);
curl_slist_free_all(list); curl_slist_free_all(list);
if (res != CURLE_OK) { if (res != CURLE_OK) {
error("POST to " + IP + " failed: " + std::string(curl_easy_strerror(res))); error("POST to " + IP + " failed: " + std::string(curl_easy_strerror(res)));
error("Curl error: " + std::string(errbuf));
return ""; return "";
} }
} else { } else {
@@ -124,25 +127,35 @@ std::string HTTP::Post(const std::string& IP, const std::string& Fields) {
return Ret; return Ret;
} }
bool HTTP::Download(const std::string& IP, const std::string& Fields, const std::string& Path) { bool HTTP::Download(const std::string& IP, const beammp_fs_string& Path, const std::string& Hash) {
static std::mutex Lock; static std::mutex Lock;
std::scoped_lock Guard(Lock); std::scoped_lock Guard(Lock);
info("Downloading an update (this may take a while)"); info("Downloading an update (this may take a while)");
std::string Ret = Get(IP, Fields); std::string Ret = Get(IP);
if (Ret.empty()) { if (Ret.empty()) {
error("Download failed"); error("Download failed");
return false; return false;
} }
std::string RetHash = Utils::GetSha256HashReallyFast(Ret, Path);
debug("Return hash: " + RetHash);
debug("Expected hash: " + Hash);
if (RetHash != Hash) {
error("Downloaded file hash does not match expected hash");
return false;
}
std::ofstream File(Path, std::ios::binary); std::ofstream File(Path, std::ios::binary);
if (File.is_open()) { if (File.is_open()) {
File << Ret; File << Ret;
File.close(); File.close();
info("Download Complete!"); info("Download Complete!");
} else { } else {
error("Failed to open file directory: " + Path); error(beammp_wide("Failed to open file directory: ") + Path);
return false; return false;
} }
@@ -156,118 +169,118 @@ void set_headers(httplib::Response& res) {
} }
void HTTP::StartProxy() { void HTTP::StartProxy() {
std::thread proxy([&]() { // std::thread proxy([&]() {
httplib::Server HTTPProxy; // httplib::Server HTTPProxy;
httplib::Headers headers = { // httplib::Headers headers = {
{ "User-Agent", "BeamMP-Launcher/" + GetVer() + GetPatch() }, // { "User-Agent", "BeamMP-Launcher/" + GetVer() + GetPatch() },
{ "Accept", "*/*" } // { "Accept", "*/*" }
}; // };
httplib::Client backend("https://backend.beammp.com"); // httplib::Client backend("https://backend.beammp.com");
httplib::Client forum("https://forum.beammp.com"); // httplib::Client forum("https://forum.beammp.com");
const std::string pattern = ".*"; // const std::string pattern = ".*";
auto handle_request = [&](const httplib::Request& req, httplib::Response& res) { // auto handle_request = [&](const httplib::Request& req, httplib::Response& res) {
set_headers(res); // set_headers(res);
if (req.has_header("X-BMP-Authentication")) { // if (req.has_header("X-BMP-Authentication")) {
headers.emplace("X-BMP-Authentication", PrivateKey); // headers.emplace("X-BMP-Authentication", PrivateKey);
} // }
if (req.has_header("X-API-Version")) { // if (req.has_header("X-API-Version")) {
headers.emplace("X-API-Version", req.get_header_value("X-API-Version")); // headers.emplace("X-API-Version", req.get_header_value("X-API-Version"));
} // }
const std::vector<std::string> path = Utils::Split(req.path, "/"); // const std::vector<std::string> path = Utils::Split(req.path, "/");
httplib::Result cli_res; // httplib::Result cli_res;
const std::string method = req.method; // const std::string method = req.method;
std::string host = ""; // std::string host = "";
if (!path.empty()) // if (!path.empty())
host = path[0]; // host = path[0];
if (host == "backend") { // if (host == "backend") {
std::string remaining_path = req.path.substr(std::strlen("/backend")); // std::string remaining_path = req.path.substr(std::strlen("/backend"));
if (method == "GET") // if (method == "GET")
cli_res = backend.Get(remaining_path, headers); // cli_res = backend.Get(remaining_path, headers);
else if (method == "POST") // else if (method == "POST")
cli_res = backend.Post(remaining_path, headers); // cli_res = backend.Post(remaining_path, headers);
} else if (host == "avatar") { // } else if (host == "avatar") {
bool error = false; // bool error = false;
std::string username; // std::string username;
std::string avatar_size = "100"; // std::string avatar_size = "100";
if (path.size() > 1) { // if (path.size() > 1) {
username = path[1]; // username = path[1];
} else { // } else {
error = true; // error = true;
} // }
if (path.size() > 2) { // if (path.size() > 2) {
try { // try {
if (std::stoi(path[2]) > 0) // if (std::stoi(path[2]) > 0)
avatar_size = path[2]; // avatar_size = path[2];
} catch (std::exception&) { } // } catch (std::exception&) { }
} // }
httplib::Result summary_res; // httplib::Result summary_res;
if (!error) { // if (!error) {
summary_res = forum.Get("/u/" + username + ".json", headers); // summary_res = forum.Get("/u/" + username + ".json", headers);
if (!summary_res || summary_res->status != 200) { // if (!summary_res || summary_res->status != 200) {
error = true; // error = true;
} // }
} // }
if (!error) { // if (!error) {
try { // try {
nlohmann::json d = nlohmann::json::parse(summary_res->body, nullptr, false); // can fail with parse_error // nlohmann::json d = nlohmann::json::parse(summary_res->body, nullptr, false); // can fail with parse_error
auto user = d.at("user"); // can fail with out_of_range // auto user = d.at("user"); // can fail with out_of_range
auto avatar_link_json = user.at("avatar_template"); // can fail with out_of_range // auto avatar_link_json = user.at("avatar_template"); // can fail with out_of_range
auto avatar_link = avatar_link_json.get<std::string>(); // auto avatar_link = avatar_link_json.get<std::string>();
size_t start_pos = avatar_link.find("{size}"); // size_t start_pos = avatar_link.find("{size}");
if (start_pos != std::string::npos) // if (start_pos != std::string::npos)
avatar_link.replace(start_pos, std::strlen("{size}"), avatar_size); // avatar_link.replace(start_pos, std::strlen("{size}"), avatar_size);
cli_res = forum.Get(avatar_link, headers); // cli_res = forum.Get(avatar_link, headers);
} catch (std::exception&) { // } catch (std::exception&) {
error = true; // error = true;
} // }
} // }
if (error) { // if (error) {
cli_res = forum.Get("/user_avatar/forum.beammp.com/user/0/0.png", headers); // cli_res = forum.Get("/user_avatar/forum.beammp.com/user/0/0.png", headers);
} // }
} else { // } else {
res.set_content("Host not found", "text/plain"); // res.set_content("Host not found", "text/plain");
return; // return;
} // }
if (cli_res) { // if (cli_res) {
res.set_content(cli_res->body, cli_res->get_header_value("Content-Type")); // res.set_content(cli_res->body, cli_res->get_header_value("Content-Type"));
} else { // } else {
res.set_content(to_string(cli_res.error()), "text/plain"); // res.set_content(to_string(cli_res.error()), "text/plain");
} // }
}; // };
HTTPProxy.Get(pattern, [&](const httplib::Request& req, httplib::Response& res) { // HTTPProxy.Get(pattern, [&](const httplib::Request& req, httplib::Response& res) {
handle_request(req, res); // handle_request(req, res);
}); // });
HTTPProxy.Post(pattern, [&](const httplib::Request& req, httplib::Response& res) { // HTTPProxy.Post(pattern, [&](const httplib::Request& req, httplib::Response& res) {
handle_request(req, res); // handle_request(req, res);
}); // });
ProxyPort = HTTPProxy.bind_to_any_port("127.0.0.1"); // ProxyPort = HTTPProxy.bind_to_any_port("127.0.0.1");
debug("HTTP Proxy listening on port " + std::to_string(ProxyPort)); // debug("HTTP Proxy listening on port " + std::to_string(ProxyPort));
HTTPProxy.listen_after_bind(); // HTTPProxy.listen_after_bind();
}); // });
proxy.detach(); // proxy.detach();
} }

View File

@@ -164,9 +164,12 @@ std::vector<char> TCPRcvRaw(SOCKET Sock, uint64_t& GRcv, uint64_t Size) {
do { do {
// receive at most some MB at a time // receive at most some MB at a time
int Len = std::min(int(Size - Rcv), 1 * 1024 * 1024); int Len = std::min(int(Size - Rcv), 1 * 1024 * 1024);
int32_t Temp = recv(Sock, &File[Rcv], Len, MSG_WAITALL); int Temp = recv(Sock, &File[Rcv], Len, MSG_WAITALL);
if (Temp < 1) { if (Temp == -1 || Temp == 0) {
info(std::to_string(Temp)); debug("Recv returned: " + std::to_string(Temp));
if (Temp == -1) {
error("Socket error during download: " + std::to_string(WSAGetLastError()));
}
UUl("Socket Closed Code 1"); UUl("Socket Closed Code 1");
KillSocket(Sock); KillSocket(Sock);
Terminate = true; Terminate = true;
@@ -177,8 +180,8 @@ std::vector<char> TCPRcvRaw(SOCKET Sock, uint64_t& GRcv, uint64_t Size) {
auto end = std::chrono::high_resolution_clock::now(); auto end = std::chrono::high_resolution_clock::now();
auto difference = end - start; auto difference = end - start;
float bits_per_s = float(Rcv * 8) / float(std::chrono::duration_cast<std::chrono::milliseconds>(difference).count()); double bits_per_s = double(Rcv * 8) / double(std::chrono::duration_cast<std::chrono::milliseconds>(difference).count());
float megabits_per_s = bits_per_s / 1000; double megabits_per_s = bits_per_s / 1000;
DownloadSpeed = megabits_per_s; DownloadSpeed = megabits_per_s;
// every 8th iteration print the speed // every 8th iteration print the speed
if (i % 8 == 0) { if (i % 8 == 0) {
@@ -298,68 +301,6 @@ void InvalidResource(const std::string& File) {
Terminate = true; Terminate = true;
} }
std::string GetSha256HashReallyFast(const std::string& filename) {
try {
EVP_MD_CTX* mdctx;
const EVP_MD* md;
uint8_t sha256_value[EVP_MAX_MD_SIZE];
md = EVP_sha256();
if (md == nullptr) {
throw std::runtime_error("EVP_sha256() failed");
}
mdctx = EVP_MD_CTX_new();
if (mdctx == nullptr) {
throw std::runtime_error("EVP_MD_CTX_new() failed");
}
if (!EVP_DigestInit_ex2(mdctx, md, NULL)) {
EVP_MD_CTX_free(mdctx);
throw std::runtime_error("EVP_DigestInit_ex2() failed");
}
std::ifstream stream(filename, std::ios::binary);
const size_t FileSize = std::filesystem::file_size(filename);
size_t Read = 0;
std::vector<char> Data;
while (Read < FileSize) {
Data.resize(size_t(std::min<size_t>(FileSize - Read, 4096)));
size_t RealDataSize = Data.size();
stream.read(Data.data(), std::streamsize(Data.size()));
if (stream.eof() || stream.fail()) {
RealDataSize = size_t(stream.gcount());
}
Data.resize(RealDataSize);
if (RealDataSize == 0) {
break;
}
if (RealDataSize > 0 && !EVP_DigestUpdate(mdctx, Data.data(), Data.size())) {
EVP_MD_CTX_free(mdctx);
throw std::runtime_error("EVP_DigestUpdate() failed");
}
Read += RealDataSize;
}
unsigned int sha256_len = 0;
if (!EVP_DigestFinal_ex(mdctx, sha256_value, &sha256_len)) {
EVP_MD_CTX_free(mdctx);
throw std::runtime_error("EVP_DigestFinal_ex() failed");
}
EVP_MD_CTX_free(mdctx);
std::string result;
for (size_t i = 0; i < sha256_len; i++) {
char buf[3];
sprintf(buf, "%02x", sha256_value[i]);
buf[2] = 0;
result += buf;
}
return result;
} catch (const std::exception& e) {
error("Sha256 hashing of '" + filename + "' failed: " + e.what());
return "";
}
}
struct ModInfo { struct ModInfo {
static std::pair<bool, std::vector<ModInfo>> ParseModInfosFromPacket(const std::string& packet) { static std::pair<bool, std::vector<ModInfo>> ParseModInfosFromPacket(const std::string& packet) {
bool success = false; bool success = false;
@@ -377,12 +318,17 @@ struct ModInfo {
.Hash = entry["hash"], .Hash = entry["hash"],
.HashAlgorithm = entry["hash_algorithm"], .HashAlgorithm = entry["hash_algorithm"],
}; };
if (entry.contains("protected")) {
modInfo.Protected = entry["protected"];
}
modInfos.push_back(modInfo); modInfos.push_back(modInfo);
success = true; success = true;
} }
} catch (const std::exception& e) { } catch (const std::exception& e) {
debug(std::string("Failed to receive mod list: ") + e.what()); debug(std::string("Failed to receive mod list: ") + e.what());
warn("Failed to receive new mod list format! This server may be outdated, but everything should still work as expected."); debug("Failed to receive new mod list format! This server may be outdated, but everything should still work as expected.");
} }
return std::make_pair(success, modInfos); return std::make_pair(success, modInfos);
} }
@@ -390,8 +336,55 @@ struct ModInfo {
size_t FileSize; size_t FileSize;
std::string Hash; std::string Hash;
std::string HashAlgorithm; std::string HashAlgorithm;
bool Protected = false;
}; };
nlohmann::json modUsage = {};
void UpdateModUsage(const std::string& fileName) {
try {
fs::path usageFile = CachingDirectory / "mods.json";
if (!fs::exists(usageFile)) {
if (std::ofstream file(usageFile); !file.is_open()) {
error("Failed to create mods.json");
return;
} else {
file.close();
}
}
std::fstream file(usageFile, std::ios::in | std::ios::out);
if (!file.is_open()) {
error("Failed to open or create mods.json");
return;
}
if (modUsage.empty()) {
auto Size = fs::file_size(CachingDirectory / "mods.json");
std::string modsJson(Size, 0);
file.read(&modsJson[0], Size);
if (!modsJson.empty()) {
auto parsedModJson = nlohmann::json::parse(modsJson, nullptr, false);
if (parsedModJson.is_object())
modUsage = parsedModJson;
}
}
modUsage[fileName] = std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::system_clock::now().time_since_epoch()).count();
file.clear();
file.seekp(0, std::ios::beg);
file << modUsage.dump();
file.close();
} catch (std::exception& e) {
error("Failed to update mods.json: " + std::string(e.what()));
}
}
void NewSyncResources(SOCKET Sock, const std::string& Mods, const std::vector<ModInfo> ModInfos) { void NewSyncResources(SOCKET Sock, const std::string& Mods, const std::vector<ModInfo> ModInfos) {
if (ModInfos.empty()) { if (ModInfos.empty()) {
CoreSend("L"); CoreSend("L");
@@ -420,22 +413,45 @@ void NewSyncResources(SOCKET Sock, const std::string& Mods, const std::vector<Mo
info("Syncing..."); info("Syncing...");
int ModNo = 1; std::vector<std::pair<std::string, std::filesystem::path>> CachedMods = {};
if (deleteDuplicateMods) {
for (const auto& entry : fs::directory_iterator(CachingDirectory)) {
const std::string filename = entry.path().filename().string();
if (entry.is_regular_file() && entry.path().extension() == ".zip" && filename.length() > 10) {
CachedMods.push_back(std::make_pair(filename.substr(0, filename.length() - 13) + ".zip", entry.path()));
}
}
}
int ModNo = 0;
int TotalMods = ModInfos.size(); int TotalMods = ModInfos.size();
for (auto ModInfoIter = ModInfos.begin(), AlsoModInfoIter = ModInfos.begin(); ModInfoIter != ModInfos.end() && !Terminate; ++ModInfoIter, ++AlsoModInfoIter) { for (auto ModInfoIter = ModInfos.begin(), AlsoModInfoIter = ModInfos.begin(); ModInfoIter != ModInfos.end() && !Terminate; ++ModInfoIter, ++AlsoModInfoIter) {
++ModNo;
if (deleteDuplicateMods) {
for (auto& CachedMod : CachedMods) {
const bool cachedModExists = CachedMod.first == ModInfoIter->FileName;
const bool cachedModIsNotNewestVersion = CachedMod.second.stem().string() + ".zip" != std::filesystem::path(ModInfoIter->FileName).stem().string() + "-" + ModInfoIter->Hash.substr(0, 8) + ".zip";
if (cachedModExists && cachedModIsNotNewestVersion) {
debug("Found duplicate mod '" + CachedMod.second.stem().string() + ".zip" + "' in cache, removing it");
std::filesystem::remove(CachedMod.second);
break;
}
}
}
if (ModInfoIter->Hash.length() < 8 || ModInfoIter->HashAlgorithm != "sha256") { if (ModInfoIter->Hash.length() < 8 || ModInfoIter->HashAlgorithm != "sha256") {
error("Unsupported hash algorithm or invalid hash for '" + ModInfoIter->FileName + "'"); error("Unsupported hash algorithm or invalid hash for '" + ModInfoIter->FileName + "'");
Terminate = true; Terminate = true;
return; return;
} }
auto FileName = std::filesystem::path(ModInfoIter->FileName).stem().string() + "-" + ModInfoIter->Hash.substr(0, 8) + std::filesystem::path(ModInfoIter->FileName).extension().string(); auto FileName = std::filesystem::path(ModInfoIter->FileName).stem().string() + "-" + ModInfoIter->Hash.substr(0, 8) + std::filesystem::path(ModInfoIter->FileName).extension().string();
auto PathToSaveTo = (fs::path(CachingDirectory) / FileName).string(); auto PathToSaveTo = (CachingDirectory / FileName);
if (fs::exists(PathToSaveTo) && GetSha256HashReallyFast(PathToSaveTo) == ModInfoIter->Hash) { if (fs::exists(PathToSaveTo) && Utils::GetSha256HashReallyFastFile(PathToSaveTo) == ModInfoIter->Hash) {
debug("Mod '" + FileName + "' found in cache"); debug("Mod '" + FileName + "' found in cache");
UpdateUl(false, std::to_string(ModNo) + "/" + std::to_string(TotalMods) + ": " + ModInfoIter->FileName);
std::this_thread::sleep_for(std::chrono::milliseconds(50)); std::this_thread::sleep_for(std::chrono::milliseconds(50));
try { try {
if (!fs::exists(GetGamePath() + "mods/multiplayer")) { if (!fs::exists(GetGamePath() / beammp_wide("mods/multiplayer"))) {
fs::create_directories(GetGamePath() + "mods/multiplayer"); fs::create_directories(GetGamePath() / beammp_wide("mods/multiplayer"));
} }
auto modname = ModInfoIter->FileName; auto modname = ModInfoIter->FileName;
#if defined(__linux__) #if defined(__linux__)
@@ -451,6 +467,43 @@ void NewSyncResources(SOCKET Sock, const std::string& Mods, const std::vector<Mo
fs::copy_file(PathToSaveTo, tmp_name, fs::copy_options::overwrite_existing); fs::copy_file(PathToSaveTo, tmp_name, fs::copy_options::overwrite_existing);
fs::rename(tmp_name, name); fs::rename(tmp_name, name);
UpdateModUsage(FileName);
} catch (std::exception& e) {
error("Failed copy to the mods folder! " + std::string(e.what()));
Terminate = true;
continue;
}
WaitForConfirm();
continue;
} else if (auto OldCachedPath = CachingDirectory / std::filesystem::path(ModInfoIter->FileName).filename();
fs::exists(OldCachedPath) && Utils::GetSha256HashReallyFastFile(OldCachedPath) == ModInfoIter->Hash) {
debug("Mod '" + FileName + "' found in old cache, copying it to the new cache");
UpdateUl(false, std::to_string(ModNo) + "/" + std::to_string(TotalMods) + ": " + ModInfoIter->FileName);
std::this_thread::sleep_for(std::chrono::milliseconds(50));
try {
fs::copy_file(OldCachedPath, PathToSaveTo, fs::copy_options::overwrite_existing);
if (!fs::exists(GetGamePath() / beammp_wide("mods/multiplayer"))) {
fs::create_directories(GetGamePath() / beammp_wide("mods/multiplayer"));
}
auto modname = ModInfoIter->FileName;
#if defined(__linux__)
// Linux version of the game doesnt support uppercase letters in mod names
for (char& c : modname) {
c = ::tolower(c);
}
#endif
debug("Mod name: " + modname);
auto name = std::filesystem::path(GetGamePath()) / "mods/multiplayer" / modname;
std::string tmp_name = name.string();
tmp_name += ".tmp";
fs::copy_file(PathToSaveTo, tmp_name, fs::copy_options::overwrite_existing);
fs::rename(tmp_name, name);
UpdateModUsage(FileName);
} catch (std::exception& e) { } catch (std::exception& e) {
error("Failed copy to the mods folder! " + std::string(e.what())); error("Failed copy to the mods folder! " + std::string(e.what()));
Terminate = true; Terminate = true;
@@ -459,10 +512,20 @@ void NewSyncResources(SOCKET Sock, const std::string& Mods, const std::vector<Mo
WaitForConfirm(); WaitForConfirm();
continue; continue;
} }
if (ModInfoIter->Protected) {
std::string message = "Mod '" + ModInfoIter->FileName + "' is protected and therefore must be placed in the Resources/Caching folder manually here: " + absolute(CachingDirectory).string();
error(message);
UUl(message);
Terminate = true;
return;
}
CheckForDir(); CheckForDir();
std::string FName = ModInfoIter->FileName; std::string FName = ModInfoIter->FileName;
do { do {
debug("Loading file '" + FName + "' to '" + PathToSaveTo + "'"); debug(beammp_wide("Loading file '") + Utils::ToWString(FName) + beammp_wide("' to '") + beammp_fs_string(PathToSaveTo) + beammp_wide("'"));
TCPSend("f" + ModInfoIter->FileName, Sock); TCPSend("f" + ModInfoIter->FileName, Sock);
std::string Data = TCPRcv(Sock); std::string Data = TCPRcv(Sock);
@@ -472,6 +535,13 @@ void NewSyncResources(SOCKET Sock, const std::string& Mods, const std::vector<Mo
break; break;
} }
if (Data != "AG") {
UUl("Received corrupted download confirmation, aborting download.");
debug("Corrupted download confirmation: " + Data);
Terminate = true;
break;
}
std::string Name = std::to_string(ModNo) + "/" + std::to_string(TotalMods) + ": " + FName; std::string Name = std::to_string(ModNo) + "/" + std::to_string(TotalMods) + ": " + FName;
std::vector<char> DownloadedFile = SingleNormalDownload(Sock, ModInfoIter->FileSize, Name); std::vector<char> DownloadedFile = SingleNormalDownload(Sock, ModInfoIter->FileSize, Name);
@@ -486,15 +556,20 @@ void NewSyncResources(SOCKET Sock, const std::string& Mods, const std::vector<Mo
OutFile.write(DownloadedFile.data(), DownloadedFile.size()); OutFile.write(DownloadedFile.data(), DownloadedFile.size());
OutFile.flush(); OutFile.flush();
} }
// 2. verify size // 2. verify size and hash
if (std::filesystem::file_size(PathToSaveTo) != DownloadedFile.size()) { if (std::filesystem::file_size(PathToSaveTo) != DownloadedFile.size()) {
error("Failed to write the entire file '" + PathToSaveTo + "' correctly (file size mismatch)"); error(beammp_wide("Failed to write the entire file '") + beammp_fs_string(PathToSaveTo) + beammp_wide("' correctly (file size mismatch)"));
Terminate = true;
}
if (Utils::GetSha256HashReallyFastFile(PathToSaveTo) != ModInfoIter->Hash) {
error(beammp_wide("Failed to write or download the entire file '") + beammp_fs_string(PathToSaveTo) + beammp_wide("' correctly (hash mismatch)"));
Terminate = true; Terminate = true;
} }
} while (fs::file_size(PathToSaveTo) != ModInfoIter->FileSize && !Terminate); } while (fs::file_size(PathToSaveTo) != ModInfoIter->FileSize && !Terminate);
if (!Terminate) { if (!Terminate) {
if (!fs::exists(GetGamePath() + "mods/multiplayer")) { if (!fs::exists(GetGamePath() / beammp_wide("mods/multiplayer"))) {
fs::create_directories(GetGamePath() + "mods/multiplayer"); fs::create_directories(GetGamePath() / beammp_wide("mods/multiplayer"));
} }
// Linux version of the game doesnt support uppercase letters in mod names // Linux version of the game doesnt support uppercase letters in mod names
@@ -505,9 +580,9 @@ void NewSyncResources(SOCKET Sock, const std::string& Mods, const std::vector<Mo
#endif #endif
fs::copy_file(PathToSaveTo, std::filesystem::path(GetGamePath()) / "mods/multiplayer" / FName, fs::copy_options::overwrite_existing); fs::copy_file(PathToSaveTo, std::filesystem::path(GetGamePath()) / "mods/multiplayer" / FName, fs::copy_options::overwrite_existing);
UpdateModUsage(FName);
} }
WaitForConfirm(); WaitForConfirm();
++ModNo;
} }
if (!Terminate) { if (!Terminate) {
@@ -551,7 +626,8 @@ void SyncResources(SOCKET Sock) {
Ret.clear(); Ret.clear();
int Amount = 0, Pos = 0; int Amount = 0, Pos = 0;
std::string PathToSaveTo, t; std::filesystem::path PathToSaveTo;
std::string t;
for (const std::string& name : FNames) { for (const std::string& name : FNames) {
if (!name.empty()) { if (!name.empty()) {
t += name.substr(name.find_last_of('/') + 1) + ";"; t += name.substr(name.find_last_of('/') + 1) + ";";
@@ -579,7 +655,7 @@ void SyncResources(SOCKET Sock) {
for (auto FN = FNames.begin(), FS = FSizes.begin(); FN != FNames.end() && !Terminate; ++FN, ++FS) { for (auto FN = FNames.begin(), FS = FSizes.begin(); FN != FNames.end() && !Terminate; ++FN, ++FS) {
auto pos = FN->find_last_of('/'); auto pos = FN->find_last_of('/');
if (pos != std::string::npos) { if (pos != std::string::npos) {
PathToSaveTo = CachingDirectory + FN->substr(pos); PathToSaveTo = CachingDirectory / std::filesystem::path(*FN).filename();
} else { } else {
continue; continue;
} }
@@ -589,23 +665,25 @@ void SyncResources(SOCKET Sock) {
if (FS->find_first_not_of("0123456789") != std::string::npos) if (FS->find_first_not_of("0123456789") != std::string::npos)
continue; continue;
if (fs::file_size(PathToSaveTo) == FileSize) { if (fs::file_size(PathToSaveTo) == FileSize) {
UpdateUl(false, std::to_string(Pos) + "/" + std::to_string(Amount) + ": " + PathToSaveTo.substr(PathToSaveTo.find_last_of('/'))); UpdateUl(false, std::to_string(Pos) + "/" + std::to_string(Amount) + ": " + PathToSaveTo.filename().string());
std::this_thread::sleep_for(std::chrono::milliseconds(50)); std::this_thread::sleep_for(std::chrono::milliseconds(50));
try { try {
if (!fs::exists(GetGamePath() + "mods/multiplayer")) { if (!fs::exists(GetGamePath() / beammp_wide("mods/multiplayer"))) {
fs::create_directories(GetGamePath() + "mods/multiplayer"); fs::create_directories(GetGamePath() / beammp_wide("mods/multiplayer"));
} }
auto modname = PathToSaveTo.substr(PathToSaveTo.find_last_of('/')); auto modname = PathToSaveTo.filename().string();
#if defined(__linux__) #if defined(__linux__)
// Linux version of the game doesnt support uppercase letters in mod names // Linux version of the game doesnt support uppercase letters in mod names
for (char& c : modname) { for (char& c : modname) {
c = ::tolower(c); c = ::tolower(c);
} }
#endif #endif
auto name = GetGamePath() + "mods/multiplayer" + modname; auto name = GetGamePath() / beammp_wide("mods/multiplayer") / Utils::ToWString(modname);
auto tmp_name = name + ".tmp"; auto tmp_name = name;
tmp_name += L".tmp";
fs::copy_file(PathToSaveTo, tmp_name, fs::copy_options::overwrite_existing); fs::copy_file(PathToSaveTo, tmp_name, fs::copy_options::overwrite_existing);
fs::rename(tmp_name, name); fs::rename(tmp_name, name);
UpdateModUsage(modname);
} catch (std::exception& e) { } catch (std::exception& e) {
error("Failed copy to the mods folder! " + std::string(e.what())); error("Failed copy to the mods folder! " + std::string(e.what()));
Terminate = true; Terminate = true;
@@ -614,12 +692,12 @@ void SyncResources(SOCKET Sock) {
WaitForConfirm(); WaitForConfirm();
continue; continue;
} else } else
remove(PathToSaveTo.c_str()); fs::remove(PathToSaveTo.wstring());
} }
CheckForDir(); CheckForDir();
std::string FName = PathToSaveTo.substr(PathToSaveTo.find_last_of('/')); std::string FName = PathToSaveTo.filename().string();
do { do {
debug("Loading file '" + FName + "' to '" + PathToSaveTo + "'"); debug("Loading file '" + FName + "' to '" + PathToSaveTo.string() + "'");
TCPSend("f" + *FN, Sock); TCPSend("f" + *FN, Sock);
std::string Data = TCPRcv(Sock); std::string Data = TCPRcv(Sock);
@@ -644,13 +722,13 @@ void SyncResources(SOCKET Sock) {
} }
// 2. verify size // 2. verify size
if (std::filesystem::file_size(PathToSaveTo) != DownloadedFile.size()) { if (std::filesystem::file_size(PathToSaveTo) != DownloadedFile.size()) {
error("Failed to write the entire file '" + PathToSaveTo + "' correctly (file size mismatch)"); error(beammp_wide("Failed to write the entire file '") + beammp_fs_string(PathToSaveTo) + beammp_wide("' correctly (file size mismatch)"));
Terminate = true; Terminate = true;
} }
} while (fs::file_size(PathToSaveTo) != std::stoull(*FS) && !Terminate); } while (fs::file_size(PathToSaveTo) != std::stoull(*FS) && !Terminate);
if (!Terminate) { if (!Terminate) {
if (!fs::exists(GetGamePath() + "mods/multiplayer")) { if (!fs::exists(GetGamePath() / beammp_wide("mods/multiplayer"))) {
fs::create_directories(GetGamePath() + "mods/multiplayer"); fs::create_directories(GetGamePath() / beammp_wide("mods/multiplayer"));
} }
// Linux version of the game doesnt support uppercase letters in mod names // Linux version of the game doesnt support uppercase letters in mod names
@@ -660,7 +738,8 @@ void SyncResources(SOCKET Sock) {
} }
#endif #endif
fs::copy_file(PathToSaveTo, GetGamePath() + "mods/multiplayer" + FName, fs::copy_options::overwrite_existing); fs::copy_file(PathToSaveTo, GetGamePath() / beammp_wide("mods/multiplayer") / Utils::ToWString(FName), fs::copy_options::overwrite_existing);
UpdateModUsage(FN->substr(pos));
} }
WaitForConfirm(); WaitForConfirm();
} }

View File

@@ -93,6 +93,8 @@ void UDPClientMain(const std::string& IP, int Port) {
ToServer->sin_port = htons(Port); ToServer->sin_port = htons(Port);
inet_pton(AF_INET, IP.c_str(), &ToServer->sin_addr); inet_pton(AF_INET, IP.c_str(), &ToServer->sin_addr);
UDPSock = socket(AF_INET, SOCK_DGRAM, 0); UDPSock = socket(AF_INET, SOCK_DGRAM, 0);
if (!magic.empty())
UDPSend(magic);
GameSend("P" + std::to_string(ClientID)); GameSend("P" + std::to_string(ClientID));
TCPSend("H", TCPSock); TCPSend("H", TCPSock);
UDPSend("p"); UDPSend("p");

View File

@@ -81,7 +81,8 @@ std::string TCPRcv(SOCKET Sock) {
UUl("Invalid Socket"); UUl("Invalid Socket");
return ""; return "";
} }
int32_t Header, Temp; int32_t Header;
int Temp;
std::vector<char> Data(sizeof(Header)); std::vector<char> Data(sizeof(Header));
Temp = recv(Sock, Data.data(), sizeof(Header), MSG_WAITALL); Temp = recv(Sock, Data.data(), sizeof(Header), MSG_WAITALL);
if (!CheckBytes(Temp)) { if (!CheckBytes(Temp)) {

View File

@@ -86,6 +86,12 @@ void InitOptions(int argc, const char *argv[], Options &options) {
options.no_download = true; options.no_download = true;
options.no_launch = true; options.no_launch = true;
options.no_update = true; options.no_update = true;
} else if (argument == "--user-path") {
if (i + 1 >= argc) {
error("You must specify a path after the `--user-path` argument");
}
options.user_path = argv[i + 1];
i++;
} else if (argument == "--" || argument == "--game") { } else if (argument == "--" || argument == "--game") {
options.game_arguments = &argv[i + 1]; options.game_arguments = &argv[i + 1];
options.game_arguments_length = argc - i - 1; options.game_arguments_length = argc - i - 1;
@@ -101,6 +107,7 @@ void InitOptions(int argc, const char *argv[], Options &options) {
"\t--no-update Skip applying launcher updates (you must update manually)\n" "\t--no-update Skip applying launcher updates (you must update manually)\n"
"\t--no-launch Skip launching the game (you must launch the game manually)\n" "\t--no-launch Skip launching the game (you must launch the game manually)\n"
"\t--dev Developer mode, same as --verbose --no-download --no-launch --no-update\n" "\t--dev Developer mode, same as --verbose --no-download --no-launch --no-update\n"
"\t--user-path <path> Path to BeamNG's User Path\n"
"\t--game <args...> Passes ALL following arguments to the game, see also `--`\n" "\t--game <args...> Passes ALL following arguments to the game, see also `--`\n"
<< std::flush; << std::flush;
exit(0); exit(0);

View File

@@ -6,8 +6,9 @@
#include <filesystem> #include <filesystem>
#include "Utils.h"
#if defined(_WIN32) #if defined(_WIN32)
#include <windows.h> #include <shlobj_core.h>
#elif defined(__linux__) #elif defined(__linux__)
#include "vdf_parser.hpp" #include "vdf_parser.hpp"
#include <pwd.h> #include <pwd.h>
@@ -23,7 +24,7 @@
#define MAX_VALUE_NAME 16383 #define MAX_VALUE_NAME 16383
int TraceBack = 0; int TraceBack = 0;
std::string GameDir; beammp_fs_string GameDir;
void lowExit(int code) { void lowExit(int code) {
TraceBack = 0; TraceBack = 0;
@@ -33,7 +34,7 @@ void lowExit(int code) {
exit(2); exit(2);
} }
std::string GetGameDir() { beammp_fs_string GetGameDir() {
#if defined(_WIN32) #if defined(_WIN32)
return GameDir.substr(0, GameDir.find_last_of('\\')); return GameDir.substr(0, GameDir.find_last_of('\\'));
#elif defined(__linux__) #elif defined(__linux__)
@@ -44,8 +45,8 @@ std::string GetGameDir() {
LONG OpenKey(HKEY root, const char* path, PHKEY hKey) { LONG OpenKey(HKEY root, const char* path, PHKEY hKey) {
return RegOpenKeyEx(root, reinterpret_cast<LPCSTR>(path), 0, KEY_READ, hKey); return RegOpenKeyEx(root, reinterpret_cast<LPCSTR>(path), 0, KEY_READ, hKey);
} }
std::string QueryKey(HKEY hKey, int ID) { std::wstring QueryKey(HKEY hKey, int ID) {
TCHAR achKey[MAX_KEY_LENGTH]; // buffer for subkey name wchar_t* achKey; // buffer for subkey name
DWORD cbName; // size of name string DWORD cbName; // size of name string
TCHAR achClass[MAX_PATH] = TEXT(""); // buffer for class name TCHAR achClass[MAX_PATH] = TEXT(""); // buffer for class name
DWORD cchClassName = MAX_PATH; // size of class string DWORD cchClassName = MAX_PATH; // size of class string
@@ -60,7 +61,7 @@ std::string QueryKey(HKEY hKey, int ID) {
DWORD i, retCode; DWORD i, retCode;
TCHAR achValue[MAX_VALUE_NAME]; wchar_t* achValue = new wchar_t[MAX_VALUE_NAME];
DWORD cchValue = MAX_VALUE_NAME; DWORD cchValue = MAX_VALUE_NAME;
retCode = RegQueryInfoKey( retCode = RegQueryInfoKey(
@@ -82,9 +83,9 @@ std::string QueryKey(HKEY hKey, int ID) {
if (cSubKeys) { if (cSubKeys) {
for (i = 0; i < cSubKeys; i++) { for (i = 0; i < cSubKeys; i++) {
cbName = MAX_KEY_LENGTH; cbName = MAX_KEY_LENGTH;
retCode = RegEnumKeyEx(hKey, i, achKey, &cbName, nullptr, nullptr, nullptr, &ftLastWriteTime); retCode = RegEnumKeyExW(hKey, i, achKey, &cbName, nullptr, nullptr, nullptr, &ftLastWriteTime);
if (retCode == ERROR_SUCCESS) { if (retCode == ERROR_SUCCESS) {
if (strcmp(achKey, "Steam App 284160") == 0) { if (wcscmp(achKey, L"Steam App 284160") == 0) {
return achKey; return achKey;
} }
} }
@@ -94,36 +95,37 @@ std::string QueryKey(HKEY hKey, int ID) {
for (i = 0, retCode = ERROR_SUCCESS; i < cValues; i++) { for (i = 0, retCode = ERROR_SUCCESS; i < cValues; i++) {
cchValue = MAX_VALUE_NAME; cchValue = MAX_VALUE_NAME;
achValue[0] = '\0'; achValue[0] = '\0';
retCode = RegEnumValue(hKey, i, achValue, &cchValue, nullptr, nullptr, nullptr, nullptr); retCode = RegEnumValueW(hKey, i, achValue, &cchValue, nullptr, nullptr, nullptr, nullptr);
if (retCode == ERROR_SUCCESS) { if (retCode == ERROR_SUCCESS) {
DWORD lpData = cbMaxValueData; DWORD lpData = cbMaxValueData;
buffer[0] = '\0'; buffer[0] = '\0';
LONG dwRes = RegQueryValueEx(hKey, achValue, nullptr, nullptr, buffer, &lpData); LONG dwRes = RegQueryValueExW(hKey, achValue, nullptr, nullptr, buffer, &lpData);
std::string data = (char*)(buffer); std::wstring data = (wchar_t*)(buffer);
std::string key = achValue; std::wstring key = achValue;
switch (ID) { switch (ID) {
case 1: case 1:
if (key == "SteamExe") { if (key == L"SteamExe") {
auto p = data.find_last_of("/\\"); auto p = data.find_last_of(L"/\\");
if (p != std::string::npos) { if (p != std::string::npos) {
return data.substr(0, p); return data.substr(0, p);
} }
} }
break; break;
case 2: case 2:
if (key == "Name" && data == "BeamNG.drive") if (key == L"Name" && data == L"BeamNG.drive")
return data; return data;
break; break;
case 3: case 3:
if (key == "rootpath") if (key == L"rootpath")
return data; return data;
break; break;
case 4: case 4:
if (key == "userpath_override") if (key == L"userpath_override")
return data; return data;
case 5: case 5:
if (key == "Local AppData") if (key == L"Local AppData")
return data; return data;
default: default:
break; break;
@@ -131,8 +133,9 @@ std::string QueryKey(HKEY hKey, int ID) {
} }
} }
} }
delete[] achValue;
delete[] buffer; delete[] buffer;
return ""; return L"";
} }
#endif #endif
@@ -159,24 +162,70 @@ void FileList(std::vector<std::string>& a, const std::string& Path) {
} }
void LegitimacyCheck() { void LegitimacyCheck() {
#if defined(_WIN32) #if defined(_WIN32)
std::string Result; wchar_t* appDataPath = new wchar_t[MAX_PATH];
std::string K3 = R"(Software\BeamNG\BeamNG.drive)"; HRESULT result = SHGetFolderPathW(NULL, CSIDL_LOCAL_APPDATA, NULL, 0, appDataPath);
HKEY hKey;
LONG dwRegOPenKey = OpenKey(HKEY_CURRENT_USER, K3.c_str(), &hKey); if (!SUCCEEDED(result)) {
if (dwRegOPenKey == ERROR_SUCCESS) { fatal("Cannot get Local Appdata directory");
Result = QueryKey(hKey, 3);
if (Result.empty()) {
debug("Failed to QUERY key HKEY_CURRENT_USER\\Software\\BeamNG\\BeamNG.drive");
lowExit(3);
}
GameDir = Result;
} else {
debug("Failed to OPEN key HKEY_CURRENT_USER\\Software\\BeamNG\\BeamNG.drive");
lowExit(4);
} }
K3.clear();
Result.clear(); auto BeamNGAppdataPath = std::filesystem::path(appDataPath) / "BeamNG";
RegCloseKey(hKey);
if (const auto beamngIniPath = BeamNGAppdataPath / "BeamNG.Drive.ini"; exists(beamngIniPath)) {
if (std::ifstream beamngIni(beamngIniPath); beamngIni.is_open()) {
std::string contents((std::istreambuf_iterator(beamngIni)), std::istreambuf_iterator<char>());
beamngIni.close();
if (contents.size() >= 3 && (unsigned char)contents[0] == 0xEF && (unsigned char)contents[1] == 0xBB && (unsigned char)contents[2] == 0xBF) {
contents = contents.substr(3);
}
auto ini = Utils::ParseINI(contents);
if (ini.empty())
lowExit(3);
else
debug("Successfully parsed BeamNG.Drive.ini");
if (ini.contains("installPath")) {
std::wstring installPath = Utils::ToWString(std::get<std::string>(ini["installPath"]));
installPath.erase(0, installPath.find_first_not_of(L" \t"));
if (installPath = std::filesystem::path(Utils::ExpandEnvVars(installPath)); std::filesystem::exists(installPath)) {
GameDir = installPath;
debug(L"GameDir from BeamNG.Drive.ini: " + installPath);
} else {
lowExit(4);
}
} else {
lowExit(5);
}
}
} else {
std::wstring Result;
std::string K3 = R"(Software\BeamNG\BeamNG.drive)";
HKEY hKey;
LONG dwRegOPenKey = OpenKey(HKEY_CURRENT_USER, K3.c_str(), &hKey);
if (dwRegOPenKey == ERROR_SUCCESS) {
Result = QueryKey(hKey, 3);
if (Result.empty()) {
debug("Failed to QUERY key HKEY_CURRENT_USER\\Software\\BeamNG\\BeamNG.drive");
lowExit(6);
}
GameDir = Result;
debug(L"GameDir from registry: " + Result);
} else {
debug("Failed to OPEN key HKEY_CURRENT_USER\\Software\\BeamNG\\BeamNG.drive");
lowExit(7);
}
K3.clear();
Result.clear();
RegCloseKey(hKey);
}
delete[] appDataPath;
#elif defined(__linux__) #elif defined(__linux__)
struct passwd* pw = getpwuid(getuid()); struct passwd* pw = getpwuid(getuid());
std::filesystem::path homeDir = pw->pw_dir; std::filesystem::path homeDir = pw->pw_dir;
@@ -184,6 +233,7 @@ void LegitimacyCheck() {
// Right now only steam is supported // Right now only steam is supported
std::vector<std::filesystem::path> steamappsCommonPaths = { std::vector<std::filesystem::path> steamappsCommonPaths = {
".steam/root/steamapps", // default ".steam/root/steamapps", // default
".steam/steam/steamapps", // Legacy Steam installations
".var/app/com.valvesoftware.Steam/.steam/root/steamapps", // flatpak ".var/app/com.valvesoftware.Steam/.steam/root/steamapps", // flatpak
"snap/steam/common/.local/share/Steam/steamapps" // snap "snap/steam/common/.local/share/Steam/steamapps" // snap
}; };
@@ -229,12 +279,9 @@ void LegitimacyCheck() {
} }
#endif #endif
} }
std::string CheckVer(const std::string& dir) { std::string CheckVer(const std::filesystem::path& dir) {
#if defined(_WIN32) std::string temp;
std::string temp, Path = dir + "\\integrity.json"; std::filesystem::path Path = dir / beammp_wide("integrity.json");
#elif defined(__linux__)
std::string temp, Path = dir + "/integrity.json";
#endif
std::ifstream f(Path.c_str(), std::ios::binary); std::ifstream f(Path.c_str(), std::ios::binary);
int Size = int(std::filesystem::file_size(Path)); int Size = int(std::filesystem::file_size(Path));
std::string vec(Size, 0); std::string vec(Size, 0);

View File

@@ -12,20 +12,23 @@
#include <nlohmann/json.hpp> #include <nlohmann/json.hpp>
#include <string> #include <string>
#if defined(_WIN32) #if defined(_WIN32)
#include <windows.h>
#elif defined(__linux__) #elif defined(__linux__)
#include <unistd.h> #include <unistd.h>
#endif #elif defined (__APPLE__)
#include <unistd.h>
#include <libproc.h>
#endif // __APPLE__
#include "Http.h" #include "Http.h"
#include "Logger.h" #include "Logger.h"
#include "Network/network.hpp" #include "Network/network.hpp"
#include "Options.h"
#include "Security/Init.h" #include "Security/Init.h"
#include "Startup.h" #include "Startup.h"
#include "Utils.h"
#include "hashpp.h" #include "hashpp.h"
#include <filesystem> #include <filesystem>
#include <fstream> #include <fstream>
#include <thread> #include <thread>
#include "Options.h"
extern int TraceBack; extern int TraceBack;
int ProxyPort = 0; int ProxyPort = 0;
@@ -72,49 +75,77 @@ Version::Version(const std::array<uint8_t, 3>& v)
: Version(v[0], v[1], v[2]) { : Version(v[0], v[1], v[2]) {
} }
std::string GetEN() { beammp_fs_string GetEN() {
#if defined(_WIN32) #if defined(_WIN32)
return "BeamMP-Launcher.exe"; return L"BeamMP-Launcher.exe";
#elif defined(__linux__) #elif defined(__linux__)
return "BeamMP-Launcher"; return "BeamMP-Launcher";
#endif #endif
} }
std::string GetVer() { std::string GetVer() {
return "2.3"; return "2.7";
} }
std::string GetPatch() { std::string GetPatch() {
return ".2"; return ".0";
} }
std::string GetEP(const char* P) { beammp_fs_string GetEP(const beammp_fs_char* P) {
static std::string Ret = [&]() { static beammp_fs_string Ret = [&]() {
std::string path(P); beammp_fs_string path(P);
return path.substr(0, path.find_last_of("\\/") + 1); return path.substr(0, path.find_last_of(beammp_wide("\\/")) + 1);
}(); }();
return Ret; return Ret;
} }
fs::path GetBP(const beammp_fs_char* P) {
fs::path fspath = {};
#if defined(_WIN32)
beammp_fs_char path[256];
GetModuleFileNameW(nullptr, path, sizeof(path));
fspath = path;
#elif defined(__linux__)
fspath = fs::canonical("/proc/self/exe");
#elif defined(__APPLE__)
pid_t pid = getpid();
char path[PROC_PIDPATHINFO_MAXSIZE];
// While this is fine for a raw executable,
// an application bundle is read-only and these files
// should instead be placed in Application Support.
proc_pidpath(pid, path, sizeof(path));
fspath = std::string(path);
#else
fspath = beammp_fs_string(P);
#endif
fspath = fs::weakly_canonical(fspath.string() + "/..");
#if defined(_WIN32)
return fspath.wstring();
#else
return fspath.string();
#endif
}
#if defined(_WIN32) #if defined(_WIN32)
void ReLaunch() { void ReLaunch() {
std::string Arg; std::wstring Arg;
for (int c = 2; c <= options.argc; c++) { for (int c = 2; c <= options.argc; c++) {
Arg += options.argv[c - 1]; Arg += Utils::ToWString(options.argv[c - 1]);
Arg += " "; Arg += L" ";
} }
info("Relaunch!"); info("Relaunch!");
system("cls"); system("cls");
ShellExecute(nullptr, "runas", (GetEP() + GetEN()).c_str(), Arg.c_str(), nullptr, SW_SHOWNORMAL); ShellExecuteW(nullptr, L"runas", (GetBP() / GetEN()).c_str(), Arg.c_str(), nullptr, SW_SHOWNORMAL);
ShowWindow(GetConsoleWindow(), 0); ShowWindow(GetConsoleWindow(), 0);
std::this_thread::sleep_for(std::chrono::seconds(1)); std::this_thread::sleep_for(std::chrono::seconds(1));
exit(1); exit(1);
} }
void URelaunch() { void URelaunch() {
std::string Arg; std::wstring Arg;
for (int c = 2; c <= options.argc; c++) { for (int c = 2; c <= options.argc; c++) {
Arg += options.argv[c - 1]; Arg += Utils::ToWString(options.argv[c - 1]);
Arg += " "; Arg += L" ";
} }
ShellExecute(nullptr, "open", (GetEP() + GetEN()).c_str(), Arg.c_str(), nullptr, SW_SHOWNORMAL); ShellExecuteW(nullptr, L"open", (GetBP() / GetEN()).c_str(), Arg.c_str(), nullptr, SW_SHOWNORMAL);
ShowWindow(GetConsoleWindow(), 0); ShowWindow(GetConsoleWindow(), 0);
std::this_thread::sleep_for(std::chrono::seconds(1)); std::this_thread::sleep_for(std::chrono::seconds(1));
exit(1); exit(1);
@@ -128,7 +159,7 @@ void ReLaunch() {
} }
info("Relaunch!"); info("Relaunch!");
system("clear"); system("clear");
int ret = execv(options.executable_name.c_str(), const_cast<char**>(options.argv)); int ret = execv((GetBP() / GetEN()).c_str(), const_cast<char**>(options.argv));
if (ret < 0) { if (ret < 0) {
error(std::string("execv() failed with: ") + strerror(errno) + ". Failed to relaunch"); error(std::string("execv() failed with: ") + strerror(errno) + ". Failed to relaunch");
exit(1); exit(1);
@@ -137,7 +168,7 @@ void ReLaunch() {
exit(1); exit(1);
} }
void URelaunch() { void URelaunch() {
int ret = execv(options.executable_name.c_str(), const_cast<char**>(options.argv)); int ret = execv((GetBP() / GetEN()).c_str(), const_cast<char**>(options.argv));
if (ret < 0) { if (ret < 0) {
error(std::string("execv() failed with: ") + strerror(errno) + ". Failed to relaunch"); error(std::string("execv() failed with: ") + strerror(errno) + ". Failed to relaunch");
exit(1); exit(1);
@@ -149,48 +180,59 @@ void URelaunch() {
void CheckName() { void CheckName() {
#if defined(_WIN32) #if defined(_WIN32)
std::string DN = GetEN(), CDir = options.executable_name, FN = CDir.substr(CDir.find_last_of('\\') + 1); std::wstring DN = GetEN(), CDir = Utils::ToWString(options.executable_name), FN = CDir.substr(CDir.find_last_of('\\') + 1);
#elif defined(__linux__) #elif defined(__linux__)
std::string DN = GetEN(), CDir = options.executable_name, FN = CDir.substr(CDir.find_last_of('/') + 1); std::string DN = GetEN(), CDir = options.executable_name, FN = CDir.substr(CDir.find_last_of('/') + 1);
#endif #endif
if (FN != DN) { if (FN != DN) {
if (fs::exists(DN)) if (fs::exists(DN))
remove(DN.c_str()); fs::remove(DN.c_str());
if (fs::exists(DN)) if (fs::exists(DN))
ReLaunch(); ReLaunch();
std::rename(FN.c_str(), DN.c_str()); fs::rename(FN.c_str(), DN.c_str());
URelaunch(); URelaunch();
} }
} }
void CheckForUpdates(const std::string& CV) { void CheckForUpdates(const std::string& CV) {
std::string requestBody = R"({"branch": ")" + Branch + R"(","pk":")" + PublicKey + R"("})"; // std::string LatestHash = HTTP::Get("https://backend.beammp.com/sha/launcher?branch=" + Branch + "&pk=" + PublicKey);
std::string LatestHash = HTTP::Get("https://backend.beammp.com/sha/launcher", requestBody); // std::string LatestVersion = HTTP::Get(
std::string LatestVersion = HTTP::Get("https://backend.beammp.com/version/launcher", requestBody); // "https://backend.beammp.com/version/launcher?branch=" + Branch + "&pk=" + PublicKey);
transform(LatestHash.begin(), LatestHash.end(), LatestHash.begin(), ::tolower); // transform(LatestHash.begin(), LatestHash.end(), LatestHash.begin(), ::tolower);
std::string EP(GetEP() + GetEN()), Back(GetEP() + "BeamMP-Launcher.back"); // beammp_fs_string BP(GetBP() / GetEN()), Back(GetBP() / beammp_wide("BeamMP-Launcher.back"));
std::string FileHash = hashpp::get::getFileHash(hashpp::ALGORITHMS::SHA2_256, EP); // std::string FileHash = Utils::GetSha256HashReallyFastFile(BP);
if (FileHash != LatestHash && IsOutdated(Version(VersionStrToInts(GetVer() + GetPatch())), Version(VersionStrToInts(LatestVersion)))) { // if (FileHash != LatestHash && IsOutdated(Version(VersionStrToInts(GetVer() + GetPatch())), Version(VersionStrToInts(LatestVersion)))) {
if (!options.no_update) { // if (!options.no_update) {
info("Launcher update found!"); // info("Launcher update " + LatestVersion + " found!");
#if defined(__linux__) // #if defined(__linux__)
error("Auto update is NOT implemented for the Linux version. Please update manually ASAP as updates contain security patches."); // error("Auto update is NOT implemented for the Linux version. Please update manually ASAP as updates contain security patches.");
#else // #else
fs::remove(Back); // info("Downloading Launcher update " + LatestHash);
fs::rename(EP, Back); // HTTP::Download(
info("Downloading Launcher update " + LatestHash); // "https://backend.beammp.com/builds/launcher?download=true"
HTTP::Download("https://backend.beammp.com/builds/launcher?download=true", requestBody, EP); // "&pk="
URelaunch(); // + PublicKey + "&branch=" + Branch,
#endif // GetBP() / (beammp_wide("new_") + GetEN()), LatestHash);
} else { // std::error_code ec;
warn("Launcher update was found, but not updating because --no-update or --dev was specified."); // fs::remove(Back, ec);
} // if (ec == std::errc::permission_denied) {
} else // error("Failed to remove old backup file: " + ec.message() + ". Using alternative name.");
info("Launcher version is up to date"); // fs::rename(BP, Back + beammp_wide(".") + Utils::ToWString(FileHash.substr(0, 8)));
TraceBack++; // } else {
// fs::rename(BP, Back);
// }
// fs::rename(GetBP() / (beammp_wide("new_") + GetEN()), BP);
// URelaunch();
// #endif
// } else {
// warn("Launcher update was found, but not updating because --no-update or --dev was specified.");
// }
// } else
// info("Launcher version is up to date. Latest version: " + LatestVersion);
// TraceBack++;
} }
@@ -227,6 +269,9 @@ void LinuxPatch() {
void InitLauncher() { void InitLauncher() {
SetConsoleTitleA(("BeamMP Launcher v" + std::string(GetVer()) + GetPatch()).c_str()); SetConsoleTitleA(("BeamMP Launcher v" + std::string(GetVer()) + GetPatch()).c_str());
SetConsoleOutputCP(CP_UTF8);
_setmode(_fileno(stdout), _O_U8TEXT);
debug("Launcher Version : " + GetVer() + GetPatch());
CheckName(); CheckName();
LinuxPatch(); LinuxPatch();
CheckLocalKey(); CheckLocalKey();
@@ -242,11 +287,11 @@ void InitLauncher() {
} }
#endif #endif
size_t DirCount(const std::filesystem::path& path) { size_t DirCount(const fs::path& path) {
return (size_t)std::distance(std::filesystem::directory_iterator { path }, std::filesystem::directory_iterator {}); return (size_t)std::distance(fs::directory_iterator { path }, fs::directory_iterator {});
} }
void CheckMP(const std::string& Path) { void CheckMP(const beammp_fs_string& Path) {
if (!fs::exists(Path)) if (!fs::exists(Path))
return; return;
size_t c = DirCount(fs::path(Path)); size_t c = DirCount(fs::path(Path));
@@ -266,7 +311,7 @@ void CheckMP(const std::string& Path) {
} }
void EnableMP() { void EnableMP() {
std::string File(GetGamePath() + "mods/db.json"); beammp_fs_string File(GetGamePath() / beammp_wide("mods/db.json"));
if (!fs::exists(File)) if (!fs::exists(File))
return; return;
auto Size = fs::file_size(File); auto Size = fs::file_size(File);
@@ -289,53 +334,55 @@ void EnableMP() {
ofs << d.dump(); ofs << d.dump();
ofs.close(); ofs.close();
} else { } else {
error("Failed to write " + File); error(beammp_wide("Failed to write ") + File);
} }
} }
} }
} }
void PreGame(const std::string& GamePath) { void PreGame(const beammp_fs_string& GamePath) {
std::string GameVer = CheckVer(GamePath); std::string GameVer = CheckVer(GamePath);
info("Game Version : " + GameVer); info("Game Version : " + GameVer);
CheckMP(GetGamePath() + "mods/multiplayer"); CheckMP(GetGamePath() / beammp_wide("mods/multiplayer"));
info("Game user path: " + GetGamePath()); info(beammp_wide("Game user path: ") + beammp_fs_string(GetGamePath()));
if (!options.no_download) { // if (!options.no_download) {
std::string requestBody = R"({"branch": ")" + Branch + R"(","pk":")" + PublicKey + R"("})"; // std::string LatestHash = HTTP::Get("https://backend.beammp.com/sha/mod?branch=" + Branch + "&pk=" + PublicKey);
std::string LatestHash = HTTP::Get("https://backend.beammp.com/sha/mod", requestBody); // transform(LatestHash.begin(), LatestHash.end(), LatestHash.begin(), ::tolower);
transform(LatestHash.begin(), LatestHash.end(), LatestHash.begin(), ::tolower); // LatestHash.erase(std::remove_if(LatestHash.begin(), LatestHash.end(),
LatestHash.erase(std::remove_if(LatestHash.begin(), LatestHash.end(), // [](auto const& c) -> bool { return !std::isalnum(c); }),
[](auto const& c) -> bool { return !std::isalnum(c); }), // LatestHash.end());
LatestHash.end());
try { // try {
if (!fs::exists(GetGamePath() + "mods/multiplayer")) { // if (!fs::exists(GetGamePath() / beammp_wide("mods/multiplayer"))) {
fs::create_directories(GetGamePath() + "mods/multiplayer"); // fs::create_directories(GetGamePath() / beammp_wide("mods/multiplayer"));
} // }
EnableMP(); // EnableMP();
} catch (std::exception& e) { // } catch (std::exception& e) {
fatal(e.what()); // fatal(e.what());
} // }
#if defined(_WIN32) // #if defined(_WIN32)
std::string ZipPath(GetGamePath() + R"(mods\multiplayer\BeamMP.zip)"); // std::wstring ZipPath(GetGamePath() / LR"(mods\multiplayer\BeamMP.zip)");
#elif defined(__linux__) // #elif defined(__linux__)
// Linux version of the game cant handle mods with uppercase names // // Linux version of the game cant handle mods with uppercase names
std::string ZipPath(GetGamePath() + R"(mods/multiplayer/beammp.zip)"); // std::string ZipPath(GetGamePath() / R"(mods/multiplayer/beammp.zip)");
#endif // #endif
std::string FileHash = hashpp::get::getFileHash(hashpp::ALGORITHMS::SHA2_256, ZipPath); // std::string FileHash = fs::exists(ZipPath) ? Utils::GetSha256HashReallyFastFile(ZipPath) : "";
if (FileHash != LatestHash) { // if (FileHash != LatestHash) {
info("Downloading BeamMP Update " + LatestHash); // info("Downloading BeamMP Update " + LatestHash);
HTTP::Download("https://backend.beammp.com/builds/client?download=true", requestBody, ZipPath); // HTTP::Download("https://backend.beammp.com/builds/client?download=true"
} // "&pk="
// + PublicKey + "&branch=" + Branch,
// ZipPath, LatestHash);
// }
std::string Target(GetGamePath() + "mods/unpacked/beammp"); // beammp_fs_string Target(GetGamePath() / beammp_wide("mods/unpacked/beammp"));
if (fs::is_directory(Target)) { // if (fs::is_directory(Target) && !fs::is_directory(Target + beammp_wide("/.git"))) {
fs::remove_all(Target); // fs::remove_all(Target);
} // }
} // }
} }

View File

@@ -9,6 +9,7 @@
#include "Network/network.hpp" #include "Network/network.hpp"
#include "Security/Init.h" #include "Security/Init.h"
#include "Startup.h" #include "Startup.h"
#include "Utils.h"
#include <curl/curl.h> #include <curl/curl.h>
#include <iostream> #include <iostream>
#include <thread> #include <thread>
@@ -37,7 +38,7 @@ int main(int argc, const char** argv) try {
curl_global_init(CURL_GLOBAL_ALL); curl_global_init(CURL_GLOBAL_ALL);
GetEP(argv[0]); GetEP(Utils::ToWString(std::string(argv[0])).c_str());
InitLog(); InitLog();
ConfigInit(); ConfigInit();