Compare commits

..

369 Commits

Author SHA1 Message Date
Tixx 89392a67c2 Bump version to 3.9.1 2026-03-08 18:16:10 +01:00
Tixx 86a1c50c8b Add dynamic export (#470)
Testing builds with -Wl,--export-dynamic

Closes #464 

---

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.
2026-03-08 18:09:42 +01:00
Tixx d97010c8ca Harden malformed vehicle packet parsing (#475)
## Summary
Reject malformed vehicle reset and paint packets before slicing
structured payloads, and catch parser exceptions in the TCP client loop.

## Problem
Or:<pid>-<vid>:null and similarly malformed Op packets could reach
substr(npos) in ParseVehicle, throwing std::out_of_range. On TCP, that
exception could escape the client loop and kill the thread.

## Changes
- Guard Or reset packets when no { payload exists
- Guard Op paint packets when no [ payload exists
- Add a small helper and unit test for structured payload extraction
- Catch std::exception around GlobalParser in the TCP client loop and
disconnect the offending client

## Verification
- git diff --check
- Full build/tests not run in this shell because cmake was not installed
2026-03-08 17:29:55 +01:00
Tixx 8a613e0b16 Prevent log spam and simplify code 2026-03-08 17:24:30 +01:00
alex 48d721f602 Harden malformed vehicle packet parsing 2026-03-07 21:23:00 -07:00
0R3Z da0089fb2e add export-dynamic flag 2026-02-19 21:15:14 +01:00
Tixx 3befc84f00 Add build deb13 (#469)
add/test Debian 13 build to github actions

Closes #464 

---

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.
2026-02-19 20:07:28 +01:00
0R3Z 48a77e9ea6 add debian 13 to release workflow 2026-02-02 12:52:42 +01:00
0R3Z f9c85a1f35 fix release workflow formatting 2026-02-02 12:51:01 +01:00
0R3Z 168a410129 add build Debian 13 for ARM64 2026-01-25 18:11:33 +01:00
0R3Z 4926e1aebd add support for Debian 13 on github workflows 2026-01-25 17:59:02 +01:00
0R3Z 1e2d8688a8 add installation scripts for Debian 13 2026-01-25 17:58:26 +01:00
Tixx 6f196aca64 Decrease likelyhood of lua stack corruption (#462)
Decreases the likelyhood of lua stack corruption.

---

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.
2026-01-18 21:56:31 +01:00
Tixx 99476a2c77 Revert "Add stack trace to server lua engine (#350)" 2026-01-15 14:44:13 +01:00
Tixx 9fa9974159 Lua version change to 5.3.5 (#458)
Update Lua version to make plugin development easier across platforms
and simplify Windows builds by using vcpkg.

---

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.
2026-01-10 21:39:52 +01:00
boubouleuh 3ef816845d Lua version change to 5.3.5 2026-01-10 21:09:05 +01:00
Tixx 6ec4106ec7 refactor: optimize string operations and improve code clarity (#451)
- Avoid redundant substr() calls in packet parsing hot-path
(TServer.cpp)
  The previous code called substr(3) twice per packet, creating
  unnecessary temporary strings. Now stores the result once.

- Replace .size() == 0 with .empty() for idiomatic C++
  (TConsole.cpp, TLuaEngine.cpp)
2026-01-02 00:42:03 +01:00
Tixx b094e35f8c Skip invalid socket when accept() fails (#457)
When Acceptor.accept() returns an error (e.g., "Too many open files"),
the server was continuing to process an invalid socket, causing resource
leaks and potential infinite error loops.
Add continue statement to skip processing when accept() fails, allowing
the server to retry on the next iteration instead of crashing.
Fixes resource exhaustion DoS vulnerability where server would enter
error loop instead of handling gracefully.
<img width="1233" height="199" alt="image"
src="https://github.com/user-attachments/assets/bad8f559-6ef2-47ee-b1c1-3e6020cdfb77"
/>

---

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-12-29 17:38:53 +01:00
wadyankaw 83afafc0c3 Skip invalid socket when accept() fails
When Acceptor.accept() returns an error (e.g., "Too many open files"),
the server was continuing to process an invalid socket, causing resource
leaks and potential infinite error loops.

Add continue statement to skip processing when accept() fails, allowing
the server to retry on the next iteration instead of crashing.

Fixes resource exhaustion DoS vulnerability where server would enter
error loop instead of handling gracefully.
2025-12-28 03:54:55 +03:00
Kipstz 31122abe10 Merge branch 'BeamMP:minor' into refactor-string-optimizations 2025-12-28 00:56:24 +01:00
Tixx 0615b57a37 Add message length validation for chat messages (#456)
Right now server only checks if chat message is empty but doesnt check
the max length. Client limits to 500 chars but if someone modifies the
client they can send huge messages. I added a check on server side to
reject messages longer than 500 characters, same as client limit. I used
translator because I don't know English well

---

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-12-28 00:35:43 +01:00
wadyankaw 4a8378427a Add message length validation for chat messages
Right now server only checks if chat message is empty but doesnt check the max length. Client limits to 500 chars but if someone modifies the client they can send huge messages.
I added a check on server side to reject messages longer than 500 characters, same as client limit.
I used translator because I don't know English well
2025-12-28 02:24:08 +03:00
Kipstz b74f0c7ca8 refactor: optimize string operations and improve code clarity
- Avoid redundant substr() calls in packet parsing hot-path (TServer.cpp)
  The previous code called substr(3) twice per packet, creating
  unnecessary temporary strings. Now stores the result once.

- Replace .size() == 0 with .empty() for idiomatic C++
  (TConsole.cpp, TLuaEngine.cpp)
2025-12-27 23:46:45 +01:00
Tixx 420c64f6cf Fix build (#444)
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-12-20 20:13:15 +01:00
Tixx add45c085b Remove debian 11 2025-12-13 00:10:30 +01:00
Tixx 372076a4ef Set fetch-depth to 0 2025-11-30 00:12:55 +01:00
Tixx eb2deb73c1 Update vcpkg 2025-11-29 23:23:36 +01:00
Tixx 21874afb87 Downgrade sol2 and force windows ver to 10 2025-11-29 23:20:27 +01:00
Tixx 184d50bf8c Update vcpkg submodule 2025-10-21 22:11:46 +02:00
Tixx c4c894c1f7 Bump version v3.9.0 2025-10-20 22:41:29 +02:00
Tixx 039a44bba5 Bump version to v3.8.5 2025-07-31 17:26:14 +02:00
Tixx add0b86b37 Implement Dialog packet and add MP.ConfirmationDialog (#427)
This PR implements a new lua function and packet used for sends dialogs
to the client.

## Example:


https://github.com/user-attachments/assets/97bb5813-ea12-4b1d-a049-2f7ebf6b6da3

Example serverside code:
```lua
--MP.ConfirmationDialog(player_id: number, title: string, body: string, buttons: object, interaction_id: string, warning: boolean = false, reportToServer: boolean = true, reportToExtensions: boolean = true)

function onChatMessage(player_id, player_name, message)
    MP.ConfirmationDialog(player_id, "Warning", "Watch your tone buddy!!", 
        {
            {
                label = "OK",
                key = "dialogOK",
                isCancel = true
            }
        }, "interactionID", true)
end

MP.RegisterEvent("onChatMessage", "onChatMessage")


function dialogOK(player_id, interaction_id)
    MP.SendChatMessage(-1, MP.GetPlayerName(player_id) .. " clicked OK")
end

MP.RegisterEvent("dialogOK", "dialogOK")
```

### Details:
Each dialog can have multiple buttons, each button having it's own
callback event (`key`).
Each dialog can also have one button with `isCancel` being true,
settings this property to true causes the button's event to be called
when the users pressed `esc` to exit out of the dialog. If a dialog is
created without any button being the cancel button then the user will
only be able to exit the dialog by restarting the session or pressing
one of the buttons.

`interaction_id` will be sent as the event data with a button press
event, to track from which dialog the button press came. As when
multiple dialogs are opened they will stack and it will become difficult
to track what button on which dialog was pressed without having multiple
event handlers.


Waiting on https://github.com/BeamMP/BeamMP/pull/715 to be merged.

---

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-26 06:52:18 +02:00
Tixx 403c1d5f78 Add support for reporting to options in ConfirmationDialog 2025-06-25 13:51:20 +02:00
Tixx 6318ca79e7 Implement Dialog packet and add MP.ConfirmationDialog 2025-06-25 13:22:05 +02:00
Tixx 2bd4ee9321 Self check functionality (#426)
This PR adds a new console command (`nettest`) that sends a request to
the server check api in order to test connectivity via the server's
public ip (serverlist entry).

- [x] https://github.com/BeamMP/ServerCheck/pull/2

---

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-24 22:21:25 +02:00
Tixx 22c0a966bb Add nettest command 2025-06-21 20:32:25 +02:00
Tixx 731599f16e Json vehicle state and apply paint packet (#416)
Converts the vehicle stored client side from a raw string to parsed json
data. This allows us to more easily edit the vehicle state serverside,
which I've started using in this PR for updating the state after a paint
packet.

---

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-19 17:46:49 +02:00
Tixx 38c6766b2b Bump version to v3.8.4 2025-06-14 20:14:42 +02:00
Tixx bcb035bafc Provider env ip (#432)
Adds `BEAMMP_PROVIDER_IP_ENV` for hosting panels, which allows the
server owner to configure which env var is read to get the ip interface
to bind to.

---

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-17 20:41:38 +02:00
Tixx 068f553fa9 Add BEAMMP_PROVIDER_IP_ENV 2025-05-17 01:04:55 +02:00
Tixx ca11f353b0 Log IP setting in debug mode 2025-05-17 01:04:05 +02:00
Tixx b7cf304d49 Client resource hash database and client resource protection (#430)
# Mod database
This PR adds a local database of mods, which is used to cache mod hashes
and protection status.

## Mod hash caching
Mod hashes will now be cached based on last write date. This will speed
up server startup because only the mods with changes will have to be
hashed.

## Mod protection
You can now protect mods! This will allow you to host a server with
copyrighted content without actually hosting the copyrighted content.
Just run `protectmod <filename with .zip> <true/false>` in the console
to protect a mod. Users that join a server with protected mods will have
to obtain the file themselves and put it in their launcher's resources
folder. The launcher will inform the user about this if the file is
missing.

## Mod reloading
You can now reload client mods while the server is running by using
`reloadmods` in the console. Keep in mind that this is mainly intended
for development, therefore it will **not** force client to rejoin and
neither will is hot-reload mods on the client.

---

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-11 01:32:19 +02:00
Tixx 03d3b873c4 Update protectmod help message
Co-authored-by: SaltySnail <51403141+SaltySnail@users.noreply.github.com>
2025-05-10 21:16:35 +02:00
Tixx ea9c808233 Bump version 2025-05-05 23:53:17 +02:00
Tixx 40bd050ca6 Prevent lua sending client events during downloading (#431)
This PR fixes an issue where players would get personal events during
downloads, which would corrupt the download and block the user from
being able to properly join the server.

---

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:20 +02:00
Tixx a0d75c01f0 Revert "support for nested lua handlers" (#428)
Reverts a PR that has been causing sol to crash.

---

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:11 +02:00
Tixx 8098431fad Prevent lua sending client events during downloading 2025-04-30 15:30:15 +02:00
Tixx 40c8c0c5c2 Add protectmod and reloadmods console commands 2025-04-26 21:15:16 +02:00
Tixx cd39f387c2 Make PluginMonitor not try to run non-lua files (#429)
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 19:21:08 -02:00
Tixx a5ca50866f Lowercase lua extension check 2025-04-19 21:59:50 +02:00
Tixx 10ea0cf59e Make PluginMonitor not try to run non-lua files 2025-04-13 13:14:25 +02:00
Tixx 7db40e068e Replace obsolete function 2025-04-01 09:43:49 +02:00
Tixx 6053aa6192 Fix protected mod kick 2025-04-01 09:11:49 +02:00
Tixx 0bb18de9f6 Check for and remove cached hashes not in folder 2025-03-31 23:55:10 +02:00
Tixx 7a439bb5b9 Add mod hash caching and mod protection 2025-03-31 08:04:15 +02:00
Tixx 6c3174ac08 Add custom IP bind option (#425)
This PR adds an option in the server config to bind to a custom IP.

- [x] Review config comment and make sure that it is not confused with
port forwarding
- [x] Validate IP addresses

---

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:39:02 +01:00
Tixx 73e9595d14 Switch C-Style int cast to c++ style uint16 cast 2025-03-29 20:17:25 +01:00
Tixx 3f7cf7a258 Bump version 2025-03-16 08:08:40 +01:00
Tixx 093310c124 Update IP config comment 2025-03-15 22:36:13 +01:00
Tixx 6286457fa4 Revert "support for nested lua handlers" 2025-03-15 22:29:37 +01:00
Tixx 71b8a61c97 Add custom IP bind option 2025-03-15 20:45:48 +01:00
Tixx f0141e4fd3 Wait for lua and other systems (#421)
This PR makes it so that connections are denied if lua hasn't loaded
yet, and makes it so that lua waits for the server to load before
accessing then uninitialized memory.
---

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-02-20 17:07:54 +01:00
Tixx 00560f7646 Wait for the server to start before loading plugins or allowing connections 2025-02-15 21:47:17 +01:00
Tixx 27d50fc2b5 Switch to github arm runners (#418)
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-02-08 21:28:09 +01:00
Tixx 52a1d9a99e Update vehicle state after paint packet 2025-01-25 22:16:17 +01:00
Tixx 2f577a2358 Store vehicles in parsed json 2025-01-25 22:16:06 +01:00
Tixx 6014536f52 Switch to github arm runners 2025-01-25 21:28:15 +01:00
Tixx fbce8a946e Add ENV for missing settings (#415)
Add ENV for missing settings.

Issue: https://github.com/BeamMP/BeamMP-Server/issues/414

Please let me know if there are any issues.

---

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-19 20:40:18 +01:00
Tixx bd9b6212e2 Bump version 2025-01-19 13:41:14 +01:00
Tixx b112ee20d8 Force IPv4 for backend requests (#417)
Force the use of ipv4 for backend requests because the launcher doesn't
support ipv6 yet

---

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-19 13:38:36 +01:00
Tixx 8593aeb21d Force IPv4 2025-01-19 11:53:42 +01:00
pedrotski 840f9b9f9d Update src/TConfig.cpp
Co-authored-by: Tixx <83774803+WiserTixx@users.noreply.github.com>
2025-01-19 06:59:35 +08:00
Tixx ec21cbbe86 Bump version 2025-01-18 21:40:07 +01:00
Tixx e90f1af109 Fix crash (#413)
This PR fixes the segfault caused by rapidjson on the first heartbeat,
and the memory leak that happens during mod 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-01-18 21:36:54 +01:00
Tixx c78775bfd8 Add size header to information packet (#409)
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:20 +01:00
pedrotski 9c3042280d Update TConfig.cpp
Add ENV for missing settings.

Issue: https://github.com/BeamMP/BeamMP-Server/issues/414

Please let me know if there are any issues.
2025-01-19 00:32:20 +08:00
Tixx 4f2ef3c3a7 Fix hashing memory leak 2025-01-17 14:40:27 +01:00
Tixx 6a2ee052ba Fix curl post headers 2025-01-16 14:22:47 +01:00
Tixx 2658d0f785 Fix segfault by switching from rapidjson to nlohmann::json 2025-01-16 14:21:43 +01:00
Tixx a7eeda0569 Add size header to information packet 2025-01-12 16:55:38 +01:00
Tixx cd29f25435 Switch to curl (#405)
This PR converts all the http client code to use curl instead of
httplib. This will hopefully do something about the connectivity issues
with the backend and auth.

---

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-12 13:00:44 +01:00
Tixx 7c864d94b3 Fix heartbeat request 2025-01-11 22:27:42 +01:00
Tixx 26f1be0a51 Switch to curl 2025-01-11 22:18:50 +01:00
Tixx d7e75ae0c7 Fix windows build 2025-01-11 22:18:19 +01:00
Tixx 1d90f53527 Add curl 2025-01-11 22:18:19 +01:00
Tixx 7dd6b41642 Debug log responses from auth and backend (#403)
Logs the reponses from authentication and backend.

---

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 22:14:42 +01:00
Tixx d7f3bc8b9f Debug log responses from auth and backend 2024-12-23 18:18:42 +01:00
Tixx 687a988701 Fix build failure with Boost >= v1.87.0 (#402)
Closes #401

Replace deprecated `ip::address::from_string` with `ip::make_address` to
avoid build errors with Boost versions >= 1.87.0

---

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.
2024-12-21 20:30:02 +01:00
Mathijs 9046b5a4d3 Fix build failure with Boost >= v1.87.0
Closes #401
2024-12-18 14:12:38 +01:00
Tixx b4d4967529 Bump version 2024-12-09 09:52:58 +01:00
Tixx 51c24b82fe remove sentry leftovers (#392)
Just cleaning up some sentry related code, mentions, etc.

---

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.
2024-11-30 21:19:48 +01:00
Tixx 5e13f9dd2d fix crash when double closing (#383) 2024-11-30 21:14:09 +01:00
Tixx f8d66c4336 Re-Add BEAMMP_PROVIDER_PORT_ENV (#399)
#398

---

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.
2024-11-30 20:23:31 +01:00
Tixx 9875defe86 Re-Add BEAMMP_PROVIDER_PORT_ENV 2024-11-30 18:04:23 +01:00
Lion fc208770dd Fix postPlayerAuth and add reason value (#395)
Closes #393 and #394
This PR cleans up and fixes the code in TNetwork::Authentication and
sends the kick reason to lua.
The event is now `function postPlayerAuth(Kicked: bool, Reason: string,
Name: string, Role: string, Guest: bool, Identifiers: table)`

---

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.
2024-11-25 10:15:20 +01:00
Tixx 99a51808a0 Fix postPlayerAuth not running until after leaving 2024-11-25 00:33:10 +01:00
Tixx 1c07cf83b2 Fix postPlayerAuth and add reason value 2024-11-24 22:28:32 +01:00
0R3Z 99136f133a remove sentry leftovers 2024-11-16 19:02:50 +01:00
Tixx 6c9d58582b Swap build instruction sequence (#391)
Swap the sequence of the build instruction steps 2 and 3 so it actually
works

No AI involved, i used my own brain to mess it up and to fix 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.
2024-11-14 22:40:26 +01:00
O1LER 4edae00998 Swap build instruction sequence 2024-11-14 18:29:30 +01:00
Lion 17e9c05f46 add PR template 2024-11-13 16:21:49 +01:00
Lion 71e3cb83ae Allow for empty icon param in MP.SendNotification (#389)
This PR allows you to call MP.SendNotification with only a player ID and
a message, removing the requirement for an icon.
2024-11-12 13:32:58 +01:00
Tixx fb2e26bd28 Allow for empty icon param in MP.SendNotification 2024-11-12 12:35:12 +01:00
Tixx 976ab68ca3 Bump version 2024-11-05 23:13:46 +01:00
Tixx 58a76e1df2 Add some information about tags in the build instructions (#386) 2024-11-05 10:30:05 +01:00
O1LER c696276fc3 Update README.md
Co-authored-by: Tixx <83774803+WiserTixx@users.noreply.github.com>
2024-11-02 23:30:22 +01:00
O1LER 6ce0608bb3 Third times the charm 2024-11-02 14:35:17 +01:00
O1LER 52ad237419 Fix bracket skill issue 2024-11-02 14:33:52 +01:00
O1LER 71038dc617 Hide links 2024-11-02 14:32:47 +01:00
O1LER dc3bb517a3 Update README.md
Co-authored-by: Tixx <83774803+WiserTixx@users.noreply.github.com>
2024-11-02 14:29:48 +01:00
O1LER 7b2c48c8d4 Add some information about tags in the build instructions 2024-11-02 14:04:09 +01:00
Lion ef557ebfc4 Fix backend heartbeat (#385) 2024-11-01 17:40:33 +01:00
Tixx b2e953b92a Fix JSON heartbeat request 2024-11-01 17:30:17 +01:00
Lion Kortlepel 576d765557 fix crash when double closing 2024-11-01 15:07:54 +01:00
Lion Kortlepel e3416804e4 bump version 2024-11-01 12:50:11 +01:00
Lion 9ad4f61209 Information packet (#382)
This PR adds an "I" packet which returns the server information. This
can be used by external programs and the launcher to get information
about a server without having to connect it to. It can be toggled in the
config and in lua.
2024-11-01 12:27:31 +01:00
Tixx aed6311146 Run clang-format on THeartbeatThread.cpp 2024-11-01 12:22:56 +01:00
Lion 5e41cefa87 Paint packet (#381)
Adds a packet for live color updating `vid:[{}]`
2024-11-01 12:18:06 +01:00
Tixx b1710ee826 Add explanation for why uuid is added later 2024-10-20 18:33:49 +02:00
Lion 03b0bd4d2c Add 'P' packet on UDP (#379)
This can be used to check if UDP works :)

I've tested this a bit with snep ingame, even spamming this on localhost
with a large number of connections seems to not impact gameplay at all.
2024-10-20 18:29:12 +02:00
Tixx 5179ac1fdc Paint packet 2024-10-20 15:38:07 +02:00
Tixx 54e31ce2ec Move backend heartbeat to json 2024-10-14 00:42:14 +02:00
Tixx 4abe9b8636 Add informationpacket setting to the config 2024-10-14 00:39:12 +02:00
Tixx 956d6f50e1 Add setting for the information packet 2024-10-14 00:39:12 +02:00
Tixx 6aeb2eb736 Add server identification packet 2024-10-14 00:39:12 +02:00
Lion Kortlepel f40d4c1ddd add 'P' packet on UDP
this can be used to check if UDP works :)
2024-10-13 22:13:05 +02:00
Lion 4f052a3e0a Make modlist an empty array by default instead of null (#377)
Related launcher PR: https://github.com/BeamMP/BeamMP-Launcher/pull/136
2024-10-12 21:06:41 +02:00
Tixx 2bf9e7d916 Make modlist [] by default instead of null 2024-10-09 19:42:47 +02:00
Lion 89c906232d Report correct client minimum version to the backend (#376) 2024-10-09 18:06:03 +02:00
Lion c39beb5b72 reuse minclientversion where possible
Co-authored-by: Tixx <83774803+WiserTixx@users.noreply.github.com>
2024-10-09 18:03:01 +02:00
Lion Kortlepel 7dd2d89ad9 clarify auth version reject message 2024-10-09 16:48:40 +02:00
Lion Kortlepel 3403c8acba fix version check on auth 2024-10-09 16:44:38 +02:00
Lion Kortlepel 0a6eecee69 report correct client minimum version to the backend 2024-10-09 16:37:16 +02:00
Lion cf3985ce00 Add Lua function to get a player's role (#366)
Adds `MP.GetPlayerRole(player_id)` to the Lua API to get a player's role
("USER", "EA", "MDEV", "STAFF", "ET") by their player id.
Currently you can only get someone's role in onPlayerAuth from the
parameters and in onVehicleSpawned and onVehicleEdited from the packet
data, but not in onPlayerJoin for example without storing it.
2024-10-05 16:09:37 +02:00
Lion Kortlepel b04c5068ea bump version 2024-10-05 16:09:02 +02:00
Lion 077bb6b1cd Add player limit bypass to onPlayerAuth (#372)
With this PR, returning 2 in onPlayerAuth will allow the player to join
without checking if the server is full. This makes it easier for plugin
developers to allow for example their staff to join without having to
change the max player count.
2024-10-05 16:07:53 +02:00
Lion 0850cde1fb Add MP.SendNotification (#373)
Adds MP.SendNotification(message, icon, category (optional) ) to the Lua
api. Uses the newly added "N" packet in the mod.
2024-10-05 16:07:27 +02:00
Lion 611e53b484 Mod hashing + better download (#374) 2024-10-04 23:29:11 +02:00
Tixx f039f57f11 Fix error messages on sendnotification 2024-10-04 20:24:30 +02:00
Lion Kortlepel 5d34090952 fix stupid read size error leading to corrupt zip 2024-09-29 01:34:38 +02:00
Lion Kortlepel 88ca17236a remove two-socket download 2024-09-29 01:15:48 +02:00
Lion Kortlepel a4b62d013c implement mod hashing + new download 2024-09-29 00:32:52 +02:00
Tixx 9a0270cb09 Return nil instead of "" when there's no client 2024-09-28 21:05:04 +02:00
Lion 55f1a3c734 Add MP.Get (#369)
Adds `MP.Get(ConfigID)` to the lua api to get the current server
settings.

```lua
lua> print(MP.Get(MP.Settings.Name]))
[LUA] BeamMP Server
lua> MP.Set(MP.Settings.Name, 'Hello World')
[INFO] Set `Name` to Hello World
lua> print(MP.Get(MP.Settings.Name))
[LUA] Hello World
```

Closes #146
2024-09-28 20:30:14 +02:00
Tixx bb3c762d68 Add player limit bypass to onPlayerAuth 2024-09-28 14:52:04 +02:00
Tixx 3ade7f5743 Add MP.SendNotification 2024-09-28 13:35:25 +02:00
Tixx 9d44c2063c Remove break after return 2024-09-22 15:34:13 +02:00
Tixx 17185da53b Add MP.Get 2024-09-21 23:17:08 +02:00
Tixx 623dfa17d5 Remove expiry check and add braces 2024-09-20 14:45:41 +02:00
Lion 7f69e336a9 Fix exception propagation on packet decompression (#365) 2024-09-20 11:39:36 +02:00
Lion f08dfc0585 fix MaxPlayers setting using value of MaxCars (#367) 2024-09-20 11:39:22 +02:00
Deer McDurr a9dee2bec5 fix MaxPlayers using value of MaxCars 2024-09-19 22:15:12 +02:00
Lion Kortlepel 5319c2878a bump version to v3.5.1 2024-09-19 17:24:57 +02:00
Lion Kortlepel 73f494041a fix exception propagation on packet decompression 2024-09-19 16:59:17 +02:00
Tixx caafb216c9 Add MP.GetPlayerRole(player_id) 2024-09-19 07:51:07 +02:00
Lion Kortlepel 530d605bc1 fix release 2024-09-19 01:54:16 +02:00
Lion 63b2a8e4a3 Add post event(s) (#364)
Adds `post*` events which are triggered after the respective `on*` event
has completed and the results have been sent.

They have the same arguments as the `on*` function, with the exception
that another argument is added in the beginning which contains whether
the `on*` variant was cancelled.
2024-09-19 01:37:17 +02:00
Lion a7a19d9a30 Fix disconnect not calling onVehicleDeleted (#336)
OnDisconnect sent a packet to the client, which was already
disconnected.
2024-09-18 18:08:46 +02:00
Lion Kortlepel 3068a0e5c4 add back car deletion 2024-09-18 16:46:11 +02:00
Lion Kortlepel f70514a021 add postVehicleEdited
why the fuck is it in past tense
2024-09-18 16:34:56 +02:00
Lion Kortlepel 94768c916d add postChatMessage 2024-09-18 16:30:49 +02:00
Lion Kortlepel 86b37e8ae1 move postPlayerAuth later again, after client insert 2024-09-18 16:26:26 +02:00
Lion Kortlepel 8f9db10474 move postPlayerAuth to after kick 2024-09-18 16:26:20 +02:00
Lion Kortlepel afa5a04043 add postPlayerAuth 2024-09-18 16:26:16 +02:00
Lion Kortlepel 4444be0af9 add postVehicleSpawn event 2024-09-18 16:26:11 +02:00
Lion 5f7207bc52 Move toml11 out of vcpkg since the toml11 authors broke it (#352) 2024-09-18 16:18:38 +02:00
Lion Kortlepel 9927d2befb add toml11 submodule 2024-09-18 15:58:55 +02:00
Lion Kortlepel 60f88916a9 remove toml11 from vcpkg.json 2024-09-18 15:51:11 +02:00
SaltySnail 0cc73e70c9 fix onVehicleDeleted not being triggered when onVehicleSpawn is triggered 2024-09-15 20:26:20 +02:00
SaltySnail bfb2086e05 fix all other places where onVehicleDeleted isn't triggered after a delete packet is sent 2024-09-15 20:26:20 +02:00
SaltySnail 9d67838f8f fix #225 2024-09-15 20:26:20 +02:00
20dka bbfb85155e fix github workflows
updated upload-artifacts action
2024-09-15 10:25:04 +01:00
Lion ce5f2e666d Download Refactoring (#356)
Limits download RAM usage (to zero) on Linux by use of `sendfile(2)`
2024-08-31 20:32:23 +02:00
Lion e076197ab2 support for non toplevel event handlers (#360)
this change replaces the `_G` event handler lookup with a lua snippet
that fetches whatever the user has set, which could be a module's
function, a builtin global, or even a function defined in the
`RegisterEvent `call
2024-08-31 20:30:02 +02:00
20dka ee03afb9a1 fix gh linux build 2024-08-29 20:17:01 +02:00
20dka f775678e2e support for nested lua handlers 2024-08-28 22:46:37 +02:00
20dka 45bb6ca6f3 fill out lua EventName 2024-08-28 22:15:57 +02:00
SaltySnail f5f6b8534d Add IPv6 support (#349)
Adds IPv6 support.

FreeBSD users beware: From this point forward, you will have to set
`sysctl net.inet6.ip6.v6only=0` so that the BeamMP-Server continues to
work. The server also prints this information.
2024-08-20 20:35:48 +02:00
Lion Kortlepel f3627ce0bf Merge branch 'fix-little-issues' into minor 2024-07-28 10:54:55 +02:00
Lion Kortlepel e1aaaf5e63 Merge branch 'fix-little-issues' into minor 2024-07-28 10:52:33 +02:00
Lion Kortlepel a147edd31a update to toml11 v3.4.1 2024-07-28 10:50:01 +02:00
Lion 214096409d Add stack trace to server lua engine (#350)
Don't worry about it
2024-07-27 19:57:37 +02:00
SaltySnail 4a062e5aa0 Fix not following naming convention
Co-authored-by: Lion <development@kortlepel.com>
2024-07-27 19:50:24 +02:00
Lion Kortlepel ba723ee106 revert fix for sol2 2024-07-16 17:51:31 +02:00
Lion Kortlepel 49ed82bea1 add more info to debug print for freebsd ipv6 support 2024-07-16 17:30:45 +02:00
Lion Kortlepel 0d848fda7c add warning about IPV6_V6ONLY=false not working on FreeBSD 2024-07-16 17:25:29 +02:00
Lion Kortlepel a0040d8c57 fix invalid sol2 linking 2024-07-16 16:58:12 +02:00
Lion baa2c86e25 fix typo in DeComp buffer size logic 2024-07-15 12:35:32 +02:00
Lion 0950d367d4 refactor decompression limits and resizing 2024-07-15 12:31:09 +02:00
Lion 8b21b6cef3 add comments to DeComp() magic numbers 2024-07-15 12:26:08 +02:00
Lion Kortlepel 82a6d4af60 repeat sendfile() until all data is sent 2024-07-14 17:01:24 +02:00
Lion Kortlepel 8b753ab6ea ignore SIGPIPE in sendfile() implementation of mod sending 2024-07-14 17:01:08 +02:00
Lion Kortlepel b097acfd4a use sendfile64 2024-07-14 16:17:39 +02:00
Lion Kortlepel 5baeaa72c2 clarify RAM requirements 2024-07-14 16:03:08 +02:00
Lion Kortlepel bd76e28ca6 use sendfile to send mods on linux 2024-07-14 15:33:26 +02:00
SaltySnail 012ce08b91 Add proper lua server stacktraces 2024-07-14 03:18:59 +02:00
Lion Kortlepel 9db3619cd8 cleanup and add comments to traceback feature 2024-07-14 01:50:31 +02:00
Lion Kortlepel 6f4c3f0ceb add traceback to lua errors by way of shitty lua hacks 2024-07-14 01:43:01 +02:00
SaltySnail 4fad047bf4 Add working stacktrace 2024-07-14 01:00:57 +02:00
SaltySnail 5502c74229 add stacktrace to the server lua engine (WIP) 2024-07-14 00:22:48 +02:00
Lion Kortlepel eaedeb5324 add IPv6 support 2024-07-12 15:45:50 +02:00
Lion 72022e3349 Refactor config, add settings command (#295)
Fix #158
2024-06-26 14:24:24 +02:00
Lucca Jiménez Könings 08374b1398 deprecate Ubuntu 20.04 2024-06-26 14:12:45 +02:00
Lucca Jiménez Könings 29f4d0d286 run clang-format 2024-06-26 14:06:06 +02:00
Lucca Jiménez Könings 3c80bcbf01 remove line ChronoWrapper.cpp:13 as discussed in review 2024-06-26 13:40:39 +02:00
Lucca Jiménez Könings 5919fc6f47 improve acl error message consistency 2024-06-26 13:38:07 +02:00
Lucca Jiménez Könings 461fb5d896 improve error messages 2024-06-26 13:34:32 +02:00
Lucca Jiménez Könings 6731b3e977 fix typo 2024-06-26 13:12:10 +02:00
Lucca Jiménez Könings e7c7f45039 fix chrono wrapper 2024-06-26 13:10:46 +02:00
Lucca Jiménez Könings 0748267fab remove superflous comments 2024-06-26 13:10:34 +02:00
Lucca Jiménez Könings 8c32d760be fix confusing error when setting wrong key 2024-06-26 13:09:18 +02:00
Lucca Jiménez Könings 7919f81927 remove dead code for deprecated config format 2024-06-26 13:08:26 +02:00
Lucca Jiménez Könings 26ef39827e fix AuthKey being writable from console 2024-06-26 13:07:58 +02:00
Lucca Jiménez Könings 2451e08b01 update remaining sections of code after merge 2024-06-26 12:31:47 +02:00
Lucca Jiménez Könings 25739cb1bd Merge branch 'minor' into 158-bug-running-settings-help-returns-nothing 2024-06-26 11:43:38 +02:00
Lucca Jiménez Könings 814927d0a1 change log output for consistency 2024-06-26 11:11:13 +02:00
Lucca Jiménez Könings 6c0a8d1d62 remove superflous code 2024-06-26 11:10:27 +02:00
Lucca Jiménez Könings 0d3256c429 Remove todo in accordance with review 2024-06-26 11:08:57 +02:00
Lucca Jiménez Könings 509225f151 Move tests from .h to .cpp 2024-06-26 11:07:46 +02:00
Lucca Jiménez Könings 73ecef1a87 Move map declarations in Settings.h into .cpp 2024-06-26 11:07:14 +02:00
Lion Kortlepel 28a9690a64 validate Ot packets 2024-06-23 21:58:32 +02:00
Lion Kortlepel 07a8d49046 fix tcp send also 2024-06-22 23:56:18 +02:00
Lion Kortlepel bfb0675efa send large packets over tcp 2024-06-22 23:51:01 +02:00
Lion Kortlepel 105fd6d4c9 rewrite compression and decompression to limit at 30 MB 2024-06-22 22:47:31 +02:00
Lion a9385c47e1 Adjust allow guests feature in heartbeat to follow Backend#33 (#341)
https://github.com/BeamMP/Backend/issues/33
2024-06-20 09:00:13 +02:00
Lion 1e9c4e357c adjust allow guests feature in heartbeat to follow Backend#33
https://github.com/BeamMP/Backend/issues/33
2024-06-20 08:58:58 +02:00
Lion a998a7c091 Reuse HTTP connections (#339)
fix #223
2024-06-16 03:10:29 +02:00
SaltySnail 277036fc52 fix not following naming convention 2024-06-16 03:00:18 +02:00
SaltySnail e776848a76 Update src/Http.cpp
Co-authored-by: Lion <development@kortlepel.com>
2024-06-16 02:53:01 +02:00
SaltySnail 63fa65e9a7 Update src/Http.cpp
Co-authored-by: Lion <development@kortlepel.com>
2024-06-16 02:52:55 +02:00
SaltySnail c07baeed1a add reusing Http connections 2024-06-16 02:45:53 +02:00
Lion 33b5384398 Add config setting to allow/deny guests (#335)
fix #247
2024-06-11 09:01:49 +02:00
SaltySnail e94cfd641d remove debug print 2024-06-10 23:16:45 +02:00
SaltySnail 6e590ff18a fix naming of the ENV 2024-06-10 23:16:16 +02:00
SaltySnail 91bc7dea79 Update src/TNetwork.cpp
Co-authored-by: Lion <development@kortlepel.com>
2024-06-10 23:12:23 +02:00
Lion 8b94b1f0ef Add an entry in serverconfig.toml for the time between update reminders (#329)
fix #321
2024-06-10 22:59:09 +02:00
SaltySnail 5dab48af92 fix #247, add allow guests config setting. 2024-06-10 22:06:09 +02:00
SaltySnail f3060f5247 change default from 3h to 30s 2024-06-08 20:24:46 +02:00
SaltySnail c61816dfeb fix 0....2h being allowed as time format 2024-06-01 23:42:44 +02:00
Lion 2fcb53530a Add more info to return to backend's /pkToUser endpoint (#332)
Features added were tested through a custom client (since @sla-ppy is
too cheap to own the game) with @lionkor while pair programming.

Fixes: #303
2024-06-01 12:25:01 +02:00
sla-ppy cee039d922 Merge branch 'BeamMP:minor' into fix_303 2024-05-30 19:15:06 +02:00
sla-ppy 566f0b55f7 remove debug info 2024-05-28 14:53:19 +02:00
sla-ppy 58a7e39419 add more info to return to pkToUser endpoint 2024-05-28 14:15:41 +02:00
SaltySnail bf7f1ef1a5 change update reminder frequency description to include an example of using a float 2024-05-25 21:59:34 +02:00
SaltySnail e35bf4fe15 change TimeFromStringWithLiteral to work with floats 2024-05-25 21:43:54 +02:00
SaltySnail 419a951c29 change wrong version number
lol

Co-authored-by: Lion <development@kortlepel.com>
2024-05-25 20:45:53 +02:00
SaltySnail 135008a73c Change time_str to be a reference
Co-authored-by: Lion <development@kortlepel.com>
2024-05-25 20:44:51 +02:00
SaltySnail 29c3fed374 fix #321 2024-05-25 20:34:33 +02:00
Lion 93192fd9b5 Fix #326 Arch Compile Issue "copy_n is not a member of 'std';" (#327)
added ```#include <algorithm>``` to Common.h, needed for "copy_n"
2024-05-24 23:01:48 +02:00
redracer eea041e8eb Fix #326 Arch Compile Issue "copy_n is not a member of 'std';"
added #include <algorithm> to Common.h, needed for "copy_n"
2024-05-24 16:10:37 -04:00
Lion a3670bff4a Add platform, lua, openssl version to version command, show lua version on startup (#325)
Closes #300
2024-05-24 12:58:30 +02:00
sla-ppy 1b60e89f26 add lua info on LuaEngine startup 2024-05-24 12:31:01 +02:00
sla-ppy 06e5805428 change version cmd behaviour 2024-05-24 12:12:20 +02:00
Lion Kortlepel 04e8d00daa fix invalid default initialization in SettingsMap 2024-05-24 09:07:54 +02:00
Lucca Jiménez Könings d30dccc94a Separate settings tests
Signed-off-by: Lucca Jiménez Könings <development@jimkoen.com>
2024-05-21 14:24:44 +02:00
Lucca Jiménez Könings 84f5f95e54 Refactor: feedback from code review
Signed-off-by: Lucca Jiménez Könings <development@jimkoen.com>
2024-05-21 13:40:33 +02:00
Lucca Jiménez Könings 67db9358e1 Fix concepts related error (for compat with gcc9)
Signed-off-by: Lucca Jiménez Könings <development@jimkoen.com>
2024-05-21 11:50:46 +02:00
Lion fd2f713485 bump version 2024-05-21 08:52:05 +02:00
Lion c880460a55 Fix macos compile Issue (#324)
Related [Issue 206](https://github.com/BeamMP/BeamMP-Server/issues/206)
2024-05-19 12:57:49 +02:00
Maximilian Rehms 7f54bcfaec successful implemented suggested change to "lua_nil"
Co-authored-by: Lion <development@kortlepel.com>
2024-05-18 22:36:57 +02:00
Maximilian Rehms 785c5343cd removed warnig suppression 2024-05-18 15:11:42 +02:00
Maximilian Rehms 40e5496819 compiles now on macos 2024-05-18 15:06:26 +02:00
Lucca Jiménez Könings 4c9fbc250a Change concepts in Settings.set + add test for remaining set overloads
Signed-off-by: Lucca Jiménez Könings <development@jimkoen.com>
2024-05-17 11:53:51 +02:00
Lion Kortlepel 31ce0cc7de add unit test + concept constraints for set(string) 2024-05-17 11:08:05 +02:00
Lion Kortlepel 3a8f4ded29 fix errors in Settings::set 2024-05-17 10:50:50 +02:00
Lucca Jiménez Könings f567645db6 Add issue with implicit argument type conversions for Settings.set()
Signed-off-by: Lucca Jiménez Könings <development@jimkoen.com>
2024-05-15 13:40:25 +02:00
Lucca Jiménez Könings 3609fd77ec Potential fix for compiler issues on Ubuntu
Signed-off-by: Lucca Jiménez Könings <development@jimkoen.com>
2024-05-15 13:15:54 +02:00
Lucca Jiménez Könings a5e3fc8fb9 Add Sync.h
Signed-off-by: Lucca Jiménez Könings <development@jimkoen.com>
2024-05-15 12:57:08 +02:00
Lucca Jiménez Könings bcd4b5a235 Fix Debug asserts on FreeBSD
Signed-off-by: Lucca Jiménez Könings <development@jimkoen.com>
2024-05-15 12:54:50 +02:00
Lucca Jiménez Könings 8c15b87628 Refactor all references to settings to use new Settings type
Signed-off-by: Lucca Jiménez Könings <development@jimkoen.com>
2024-05-15 12:52:09 +02:00
Lucca Jiménez Könings 13e641b3a3 Remove interfering legacy code (http,password,etc)
Signed-off-by: Lucca Jiménez Könings <development@jimkoen.com>
2024-05-15 12:45:19 +02:00
Lion 5f9726f10f Use hard disconnect instead of ClientKick in timeout (#320) 2024-05-11 13:00:25 +02:00
Lion Kortlepel fcd408970b always initialize ping timer 2024-05-11 12:44:41 +02:00
Lion cf5ebcbd1a Fix lua number (int vs double) handling, add lua unit tests for json encode + decode, fix empty array or table serializing to null (#319) 2024-05-11 12:19:13 +02:00
Lion c9d926f9e3 Fix Lua assert error when adding values to tables (e.g. in event arguments) (#318)
When adding elements to a table, the .add() function does not behave as
expected in various cases, making it really shit and difficult to use.
Instead, we keep our own index and just add one by one. It works, and
it's easy to understand.

This may lead to indices which are nil, i.e. non-fully-sequential
tables, but I can't be asked to worry about it because that shouldn't be
an issue when we use .set() everywhere.
2024-05-11 12:18:56 +02:00
Lion Kortlepel 9f47978f0f use hard disconnect insteadof clientkick 2024-05-11 12:17:02 +02:00
Lion Kortlepel a0f649288e fix lua number handling, add lua unit tests for json encode + decode 2024-05-10 15:54:52 +02:00
Lion Kortlepel b995a222ff bump version 2024-05-10 14:57:22 +02:00
Lion Kortlepel c5dff8b913 fix lua assertion on event argument passing
When adding elements to a table, the .add() function does not behave as
expected in various cases, making it really shit and difficult to use.
Instead, we keep our own index and just add one by one. It works, and
it's easy to understand.

This may lead to indices which are nil, i.e. non-fully-sequential
tables, but I can't be asked to worry about it because that shouldn't be
an issue when we use .set() everywhere.
2024-05-10 14:45:06 +02:00
Lion 0c740ccedf bump version 2024-05-08 12:01:33 +02:00
Lion b0cf5c7838 Fix lua assertions crashing the server (#198)
The server would crash on a Lua plugin ASSERT() - this now instead
prints an error. May still lead to weird behavior, but less weird than a
crash.
2024-05-08 11:48:56 +02:00
Lion 586510041d fix TriggerGlobalEvent not passing event arguments correctly (#307)
This is supposed to close #106 (The code is by @lionkor from
https://github.com/BeamMP/BeamMP-Server/commit/b068a9b48f21cc80c6aa2ae3580fc9a5e3d411e0)
2024-05-08 11:47:23 +02:00
Lion 1794c3fe45 Add Lua execution profiler Util.DebugExecutionTime() (#267)
Adds `Util.DebugExecutionTime()`, which returns a table of
`function_name: milliseconds`, in which each function's execution time
is averaged over all time.

You can call this function like so:
```lua
-- event to print the debug times
MP.RegisterEvent("printStuff", "printStuff")
-- prints the execution time of all event handlers
function printStuff()
	print(Util.DebugExecutionTime())
end
-- run every 5 seconds (or 10, or 60, whatever makes sense for you
MP.CreateEventTimer("printStuff", 5000)
```

Pretty print function:
```lua
function printDebugExecutionTime()
    local stats = Util.DebugExecutionTime()
    local pretty = "DebugExecutionTime:\n"
    local longest = 0
    for name, t in pairs(stats) do
        if #name > longest then
            longest = #name
        end
    end
    for name, t in pairs(stats) do
        pretty = pretty .. string.format("%" .. longest + 1 .. "s: %12f +/- %12f (min: %12f, max: %12f) (called %d time(s))\n", name, t.mean, t.stdev, t.min, t.max, t.n)
    end
    print(pretty)
end
```

`Util.DebugExecutionTime()` returns a table, where each key is an event
handler function name, and each value is a table consisting of `mean`
(simple average), `stddev` (standard deviation aka mean of the
variance), `min` and `max`, all in milliseconds, as well as `n` as the
number of samples taken.
2024-05-08 11:46:02 +02:00
Lion ee599e970d Make Plugin load order deterministic (alphabetic) (#315)
Add Feature / Fix: #306
2024-05-08 11:31:34 +02:00
Lion Kortlepel 40158dc252 fix compile error 2024-05-08 11:31:12 +02:00
Neptnium 5ece60574a Make Plugin load order deterministic
fix typo "mResourceServerPath" named "mFolder"

Fix typo number 2
2024-05-08 11:31:05 +02:00
Lucca Jiménez Könings ff812429ed Fix settings set not accepting boolean literals
Signed-off-by: Lucca Jiménez Könings <development@jimkoen.com>
2024-05-07 18:16:06 +02:00
Lucca Jiménez Könings 639c46ad36 Remove additional debug calls
Signed-off-by: Lucca Jiménez Könings <development@jimkoen.com>
2024-05-07 18:16:06 +02:00
Lucca Jiménez Könings 158875a962 Remove debug code for new Settings implementation
Signed-off-by: Lucca Jiménez Könings <development@jimkoen.com>
2024-05-07 18:16:06 +02:00
Lucca Jiménez Könings c6dac55679 Wrap Settings into synchronization primitives
Signed-off-by: Lucca Jiménez Könings <development@jimkoen.com>
2024-05-07 18:16:06 +02:00
Lucca Jiménez Könings 37109ae3f1 Make 'General::Debug' setting r/w from runtime
Signed-off-by: Lucca Jiménez Könings <development@jimkoen.com>
2024-05-07 18:16:05 +02:00
Lucca Jiménez Könings d6b78b9683 Fix argument parsing at wrong index
Signed-off-by: Lucca Jiménez Könings <development@jimkoen.com>
2024-05-07 18:16:05 +02:00
Lucca Jiménez Könings f5e2f7425f Fix ComposedKey metadata not printing to console
Make ComposedKey formattable by overloading fmt::format

Signed-off-by: Lucca Jiménez Könings <development@jimkoen.com>
2024-05-07 18:16:05 +02:00
Lucca Jiménez Könings 8693b8a2b0 Add list argument to command settings
Signed-off-by: Lucca Jiménez Könings <development@jimkoen.com>
2024-05-07 18:16:05 +02:00
Lucca Jiménez Könings 3989961ff9 Add get and set to console command settings
Signed-off-by: Lucca Jiménez Könings <development@jimkoen.com>
2024-05-07 18:16:05 +02:00
Lucca Jiménez Könings a357ff8ca3 Add missing options to defaults
Signed-off-by: Lucca Jiménez Könings <development@jimkoen.com>
2024-05-07 18:16:05 +02:00
Lucca Jiménez Könings 86f0052e07 Add rudimentary access control to type Settings
Signed-off-by: Lucca Jiménez Könings <development@jimkoen.com>
2024-05-07 18:16:05 +02:00
Lucca Jiménez Könings 89034a64e0 Add logic for new Settings type
Signed-off-by: Lucca Jiménez Könings <development@jimkoen.com>
2024-05-07 18:16:05 +02:00
Lucca Jiménez Könings 55f5437618 Add new type Settings & refactor TSettings behavior
into Settings by adding getters/setters

Signed-off-by: Lucca Jiménez Könings <development@jimkoen.com>
2024-05-07 18:16:05 +02:00
Neptnium c605498a2d Merge branch 'BeamMP:minor' into minor 2024-05-07 08:26:44 +02:00
Lion 4d7967d5d9 bump version (#314) 2024-05-06 14:37:50 +02:00
Lion Kortlepel 5dd4c97ed1 bump version 2024-05-06 14:37:34 +02:00
Lion 13390c5388 Remove backup1, backup2 backend endpoints (#313) 2024-05-06 12:05:30 +02:00
Lion Kortlepel 2f995a71ae remove backup1, backup2 endpoints 2024-05-06 12:04:56 +02:00
Lion 3b5c6491a3 Fix timeout for synced players (#289)
Fix: #275.

While I was at it, I also refactored the debug output so its more
appealing to read.

I dont see further ways to improve on this currently.
2024-05-06 12:02:31 +02:00
sla-ppy db8719b5cd fix timeout for synced players 2024-05-06 12:02:16 +02:00
Lion a3c4b82a5d Allow provider to define server port ENV via BEAMMP_PROVIDER_PORT_ENV (#244) 2024-05-06 11:59:28 +02:00
Lion Kortlepel 3b0e49fb06 add /bigobj to fix compiling on windows 2024-05-06 11:56:26 +02:00
Lion Kortlepel cf8f10b949 add --port=value argument 2024-05-06 11:56:26 +02:00
Lion Kortlepel 6f50cad76b add BEAMMP_PROVIDER_PORT_ENV to allow provider to change which ENV
variable the port is fetched from
2024-05-06 11:56:26 +02:00
Lion 03d91b1f4d Disable server config generation via ENV variable BEAMMP_PROVIDER_DISABLE_CONFIG (#240) 2024-05-06 11:53:57 +02:00
Lion Kortlepel e407d246e2 update vcpkg 2024-05-06 11:47:00 +02:00
Lion Kortlepel 234bdf5877 add ENV variable to disable config generation and parsing 2024-05-06 11:46:21 +02:00
Lion c3b4528c89 Add lua Util.LogInfo(), Util.LogError(), etc. functions (#309)
```lua
Util.LogInfo("Hello, World!")
Util.LogWarn("Cool warning")
Util.LogError("Oh no!")
Util.LogDebug("hi")
```
now produces

```
[19/04/24 11:06:50.142] [Test] [INFO] Hello, World!    
[19/04/24 11:06:50.142] [Test] [WARN] Cool warning    
[19/04/24 11:06:50.142] [Test] [ERROR] Oh no!
[19/04/24 11:06:50.142] [Test] [DEBUG] hi
```

where `Test` is the lua state id.
2024-05-06 11:39:12 +02:00
Lion Kortlepel 5cf6d1d228 add Util.LogDebug 2024-04-20 20:16:57 +02:00
Lion Kortlepel 65017d0834 add lua log to test common 2024-04-19 11:35:39 +02:00
Lion Kortlepel 1237e7e533 add lua logging facilities as Util.Log*() functions 2024-04-19 11:10:50 +02:00
Neptnium d81087b5af fix TriggerGlobalEvent not passing event arguments correctly (closes #106) 2024-04-18 18:43:04 +02:00
Lion Kortlepel 7e1f86478d make mutex mutable 2024-03-07 01:03:16 +01:00
Lion Kortlepel c85e026c2d cleanup 2024-03-07 00:45:00 +01:00
Lion Kortlepel e4826e8bf1 remove test code 2024-03-07 00:42:36 +01:00
Lion Kortlepel 590b159f14 switch to a runnning calculation for stdev and mean 2024-03-07 00:25:47 +01:00
Lion Kortlepel 40533c04bc add total calls to stats 2024-03-06 10:27:22 +01:00
Lion Kortlepel 946c1362e1 add custom profiling via Debug(Start|Stop)Profile 2024-03-06 10:27:22 +01:00
Lion Kortlepel 4347cb4af2 add min, max, stddev to stats 2024-03-06 10:27:22 +01:00
Lion Kortlepel 88721d4f7f add boost circular buffer to dependencies 2024-03-06 10:27:22 +01:00
Lion Kortlepel 7deea900fb add Util.DebugExecutionTime 2024-03-06 10:27:22 +01:00
Lion Kortlepel cbc1483537 add profiling code 2024-03-06 10:27:22 +01:00
Lion e2d7721438 Update lionkor/commandline to v2.4.2 (#265)
This fixes stdin/stdout redirect on windows.
2024-02-24 20:41:17 +01:00
Lion 0146a1cbe8 Housekeeping (#293) 2024-02-24 20:36:12 +01:00
Lion Kortlepel 352a3aa536 update contributing 2024-02-15 17:38:21 +01:00
Lion Kortlepel ac8c386c61 run gh workflows on PR and push differently 2024-02-15 17:38:21 +01:00
Lion Kortlepel d1fb15fc9a run ci/cd on PR open, reopen and ready for review 2024-02-15 17:38:21 +01:00
Lion Kortlepel f376d9f7be update vcpkg 2024-02-15 17:38:21 +01:00
Lion Kortlepel 2f3dcfeb5a update commandline 2024-02-15 17:38:21 +01:00
Lion 913ba1c793 Link to the docs instead of the wiki in ServerConfig.toml (#291)
Changed the update message as well.
Closes #290
2024-02-15 12:38:48 +01:00
Lion 9a0c9d3ce4 Fix unicycle counting as car (#288)
Fixes #246.
2024-02-15 10:13:49 +01:00
sla-ppy a0d9be18b7 documentate unicycle dirty fix 2024-02-14 13:51:37 +01:00
alex.ita 87d2e3355a Update Common.cpp 2024-02-14 11:56:02 +01:00
alex.ita afb3763eab Update TConfig.cpp 2024-02-14 11:51:24 +01:00
sla-ppy 9de5a08b0c dirty fix unicycle counting as car 2024-02-12 01:24:17 +01:00
Lion bef623a281 Fix assigment of ID's so two unique users dont get the same ID (#277)
Closes #154
2024-02-08 23:58:18 +01:00
sla-ppy e852245bae add mutex to openid 2024-02-08 23:29:10 +01:00
Lion 5f5af9b0a7 Add version command (#274)
closes #148
2024-02-07 21:51:41 +01:00
sla-ppy 64f2e08ed2 add the letter v 2024-02-07 21:46:18 +01:00
sla-ppy 707682f4a8 refactor version 2024-02-07 20:47:01 +01:00
sla-ppy 913674740d add version cmd 2024-02-07 16:07:22 +01:00
sla-ppy 1c575ff1bc add /bigobj 2024-02-07 16:06:19 +01:00
Lion e72c217e63 Remove all unused platforms from FUNDING.yml 2024-01-25 12:14:25 +01:00
Lion 75bae8ee5a Create FUNDING.yml 2024-01-25 12:14:25 +01:00
Lambdax 594c7881e5 Removing proprietary copyright notice from readme.md (#266) 2024-01-24 13:50:21 +01:00
Lion b9c9f22feb Update README.md 2024-01-23 21:04:37 +01:00
Lion a3ca9573a5 Update LICENSE 2024-01-23 21:00:11 +01:00
Lion Kortlepel aff537afdf add AGPL-3.0 license header 2024-01-23 21:00:11 +01:00
Lion Kortlepel acc2dd29e9 update lionkor/commandline to v2.4.2
this fixes stdin/stdout redirect on windows
2024-01-23 19:19:29 +01:00
Lucca Jiménez Könings c77df3659a fix typos and phrasing in README.md
Signed-off-by: Lucca Jiménez Könings <development@jimkoen.com>
2024-01-19 14:29:05 +01:00
Lucca Jiménez Könings fff9580f66 remove unecessary comment
Signed-off-by: Lucca Jiménez Könings <development@jimkoen.com>
2024-01-19 14:29:05 +01:00
Lucca Jiménez Könings 9948f2412d move undef macros into Compat.h
Signed-off-by: Lucca Jiménez Könings <development@jimkoen.com>
2024-01-19 14:29:05 +01:00
Lucca Jiménez Könings 6ca4acbbf8 update README.md
Signed-off-by: Lucca Jiménez Könings <development@jimkoen.com>
2024-01-19 14:29:05 +01:00
Lucca Jiménez Könings df5766a935 document FreeBSD specific undefs
Signed-off-by: Lucca Jiménez Könings <development@jimkoen.com>
2024-01-19 14:29:05 +01:00
Lucca Jiménez Könings 2a54d448c3 fix typo
Signed-off-by: Lucca Jiménez Könings <development@jimkoen.com>
2024-01-19 14:29:05 +01:00
Lucca Jiménez Könings 3ce790a820 update README.md
Signed-off-by: Lucca Jiménez Könings <development@jimkoen.com>
2024-01-19 14:29:05 +01:00
Lucca Jiménez Könings 2374eba750 fix tests on FreeBSD
Signed-off-by: Lucca Jiménez Könings <development@jimkoen.com>
2024-01-19 14:29:05 +01:00
Lucca Jiménez Könings f82572077f add FreeBSD build target
Signed-off-by: Lucca Jiménez Könings <development@jimkoen.com>
2024-01-19 14:29:05 +01:00
Lion 39badf432d remove whitespace 2024-01-17 13:15:30 +01:00
Lion Kortlepel 9915c83363 fix release arm64 workflow 2024-01-15 14:49:17 +01:00
Lion Kortlepel 9f5a30a871 fix windows build wrongly calling vcpg from the submodule 2024-01-15 14:49:17 +01:00
Lion Kortlepel 6832fd05d6 remove --build-id=none from build flags 2024-01-15 14:49:17 +01:00
Lion Kortlepel 03307e71fb use a beammp_lua_errorf instead of a std::terminate on sol2 assertion failure 2023-12-05 18:25:27 +01:00
89 changed files with 6394 additions and 4383 deletions
+6
View File
@@ -0,0 +1,6 @@
Please replace this text <-> with your PR description and leave the below declarations intact.
---
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.
+24 -19
View File
@@ -1,9 +1,13 @@
name: Linux
on: [push]
on:
push:
branches:
- "develop"
- "minor"
pull_request:
env:
env:
VCPKG_BINARY_SOURCES: "clear;x-gha,readwrite"
VCPKG_FORCE_SYSTEM_BINARIES: 1
CMAKE_BUILD_TYPE: "Release"
@@ -15,20 +19,20 @@ jobs:
strategy:
matrix:
include:
- distro: debian
version: 11
- distro: debian
version: 12
- distro: debian
version: 13
- distro: ubuntu
version: 22.04
- distro: ubuntu
version: 20.04
version: 24.04
container:
image: ${{ matrix.distro }}:${{ matrix.version }}
steps:
- name: get-cmake
uses: lukka/get-cmake@v3.28.1
- name: Export GitHub Actions cache environment variables
uses: actions/github-script@v6
with:
@@ -43,7 +47,8 @@ jobs:
- uses: actions/checkout@v2
with:
submodules: 'recursive'
submodules: "recursive"
fetch-depth: 0
- name: Git config safe directory
shell: bash
@@ -59,13 +64,13 @@ jobs:
run: bash ./scripts/${{ matrix.distro }}-${{ matrix.version }}/3-build.sh
- name: Archive server artifact
uses: actions/upload-artifact@v2
uses: actions/upload-artifact@v4
with:
name: BeamMP-Server.${{ matrix.distro }}.${{ matrix.version }}.x86_64
path: ./bin/BeamMP-Server
- name: Archive server debug info artifact
uses: actions/upload-artifact@v2
uses: actions/upload-artifact@v4
with:
name: debuginfo.${{ matrix.distro }}.${{ matrix.version }}.x86_64
path: ./bin/BeamMP-Server.debug
@@ -80,26 +85,26 @@ jobs:
run: ./bin/BeamMP-Server-tests
arm64-matrix:
runs-on: [Linux, ARM64]
runs-on: ubuntu-22.04-arm
env:
VCPKG_DEFAULT_TRIPLET: "arm64-linux"
strategy:
matrix:
include:
- distro: debian
version: 11
- distro: debian
version: 12
- distro: debian
version: 13
- distro: ubuntu
version: 22.04
- distro: ubuntu
version: 20.04
version: 24.04
container:
image: ${{ matrix.distro }}:${{ matrix.version }}
steps:
- name: get-cmake
uses: lukka/get-cmake@v3.28.1
- name: Export GitHub Actions cache environment variables
uses: actions/github-script@v6
with:
@@ -114,7 +119,8 @@ jobs:
- uses: actions/checkout@v2
with:
submodules: 'recursive'
submodules: "recursive"
fetch-depth: 0
- name: Git config safe directory
shell: bash
@@ -130,13 +136,13 @@ jobs:
run: bash ./scripts/${{ matrix.distro }}-${{ matrix.version }}/3-build.sh
- name: Archive server artifact
uses: actions/upload-artifact@v2
uses: actions/upload-artifact@v4
with:
name: BeamMP-Server.${{ matrix.distro }}.${{ matrix.version }}.arm64
path: ./bin/BeamMP-Server
- name: Archive server debug info artifact
uses: actions/upload-artifact@v2
uses: actions/upload-artifact@v4
with:
name: debuginfo.${{ matrix.distro }}.${{ matrix.version }}.arm64
path: ./bin/BeamMP-Server.debug
@@ -149,4 +155,3 @@ jobs:
- name: Test
run: ./bin/BeamMP-Server-tests
+21 -18
View File
@@ -3,7 +3,7 @@ on:
push:
# Sequence of patterns matched against refs/tags
tags:
- 'v*' # Push events to matching v*, i.e. v1.0, v20.15.10
- "v*" # Push events to matching v*, i.e. v1.0, v20.15.10
env:
VCPKG_BINARY_SOURCES: "clear;x-gha,readwrite"
@@ -14,8 +14,8 @@ jobs:
create-release:
runs-on: ubuntu-latest
name: Create Release
outputs:
upload_url: ${{ steps.create_release.outputs.upload_url }}
outputs:
upload_url: ${{ steps.create_release.outputs.upload_url }}
steps:
- name: Create Release
id: create_release
@@ -38,20 +38,20 @@ jobs:
strategy:
matrix:
include:
- distro: debian
version: 11
- distro: debian
version: 12
- distro: debian
version: 13
- distro: ubuntu
version: 22.04
- distro: ubuntu
version: 20.04
version: 24.04
container:
image: ${{ matrix.distro }}:${{ matrix.version }}
steps:
- name: get-cmake
uses: lukka/get-cmake@v3.28.1
- name: Export GitHub Actions cache environment variables
uses: actions/github-script@v6
with:
@@ -66,7 +66,8 @@ jobs:
- uses: actions/checkout@v2
with:
submodules: 'recursive'
submodules: "recursive"
fetch-depth: 0
- name: Git config safe directory
shell: bash
@@ -82,7 +83,7 @@ jobs:
run: bash ./scripts/${{ matrix.distro }}-${{ matrix.version }}/3-build.sh
- name: Upload Release Asset
id: upload-release-asset
id: upload-release-asset
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
@@ -104,19 +105,19 @@ jobs:
asset_content_type: application/x-elf
arm64-matrix:
runs-on: [Linux, ARM64]
runs-on: ubuntu-22.04-arm
needs: create-release
strategy:
matrix:
include:
- distro: debian
version: 11
- distro: debian
version: 12
- distro: debian
version: 13
- distro: ubuntu
version: 22.04
- distro: ubuntu
version: 20.04
version: 24.04
env:
VCPKG_DEFAULT_TRIPLET: "arm64-linux"
VCPKG_FORCE_SYSTEM_BINARIES: 1
@@ -125,7 +126,7 @@ jobs:
steps:
- name: get-cmake
uses: lukka/get-cmake@v3.28.1
- name: Export GitHub Actions cache environment variables
uses: actions/github-script@v6
with:
@@ -140,7 +141,8 @@ jobs:
- uses: actions/checkout@v2
with:
submodules: 'recursive'
submodules: "recursive"
fetch-depth: 0
- name: Git config safe directory
shell: bash
@@ -156,7 +158,7 @@ jobs:
run: bash ./scripts/${{ matrix.distro }}-${{ matrix.version }}/3-build.sh
- name: Upload Release Asset
id: upload-release-asset
id: upload-release-asset
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
@@ -193,7 +195,8 @@ jobs:
- uses: actions/checkout@v2
with:
submodules: 'recursive'
submodules: "recursive"
fetch-depth: 0
- name: Create Build Environment
shell: bash
@@ -204,7 +207,7 @@ jobs:
run: bash ./scripts/windows/2-build.sh
- name: Upload Release Asset
id: upload-release-asset
id: upload-release-asset
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+8 -2
View File
@@ -1,6 +1,11 @@
name: Windows
on: [push]
on:
push:
branches:
- 'develop'
- 'minor'
pull_request:
env:
VCPKG_DEFAULT_TRIPLET: x64-windows-static
@@ -21,6 +26,7 @@ jobs:
- uses: actions/checkout@v2
with:
submodules: 'recursive'
fetch-depth: 0
- name: Setup vcpkg
uses: lukka/run-vcpkg@v11
@@ -36,7 +42,7 @@ jobs:
run: bash ./scripts/windows/2-build.sh
- name: Archive server artifact
uses: actions/upload-artifact@v2
uses: actions/upload-artifact@v4
with:
name: BeamMP-Server-windows
path: ./bin/Release/BeamMP-Server.exe
-1
View File
@@ -1,5 +1,4 @@
.idea/
.sentry-native/
*.orig
*.toml
boost_*
+3 -3
View File
@@ -4,6 +4,6 @@
[submodule "vcpkg"]
path = vcpkg
url = https://github.com/Microsoft/vcpkg.git
[submodule "deps/BeamMP-Protocol"]
path = deps/BeamMP-Protocol
url = https://github.com/BeamMP/BeamMP-Protocol
[submodule "deps/toml11"]
path = deps/toml11
url = https://github.com/ToruNiina/toml11
+38 -26
View File
@@ -15,8 +15,6 @@ include(cmake/StandardSettings.cmake)
include(cmake/StaticAnalyzers.cmake)
include(cmake/Git.cmake)
add_subdirectory(deps/BeamMP-Protocol)
# below are options which should be changed
### SETTINGS ###
@@ -25,49 +23,61 @@ add_subdirectory(deps/BeamMP-Protocol)
set(PRJ_HEADERS
include/ArgsParser.h
include/BoostAliases.h
include/Client.h
include/Common.h
include/Compat.h
include/Cryptography.h
include/CustomAssert.h
include/Defer.h
include/Environment.h
include/FileWatcher.h
include/Http.h
include/IThreaded.h
include/Json.h
include/LuaAPI.h
include/LuaPlugin.h
include/Plugin.h
include/PluginManager.h
include/RWMutex.h
include/SignalHandling.h
include/TConfig.h
include/TConsole.h
include/THeartbeatThread.h
include/TLuaEngine.h
include/TLuaPlugin.h
include/TNetwork.h
include/TPluginMonitor.h
include/TPPSMonitor.h
include/TResourceManager.h
include/TScopedTimer.h
include/Value.h
include/TServer.h
include/VehicleData.h
include/Network.h
include/Env.h
include/Settings.h
include/Profiling.h
include/ChronoWrapper.h
)
# add all source files (.cpp) to this, except the one with main()
set(PRJ_SOURCES
src/ArgsParser.cpp
src/Client.cpp
src/Common.cpp
src/Compat.cpp
src/FileWatcher.cpp
src/Http.cpp
src/LuaAPI.cpp
src/LuaPlugin.cpp
src/SignalHandling.cpp
src/TConfig.cpp
src/TConsole.cpp
src/THeartbeatThread.cpp
src/TLuaEngine.cpp
src/TLuaPlugin.cpp
src/TNetwork.cpp
src/TPluginMonitor.cpp
src/TPPSMonitor.cpp
src/TResourceManager.cpp
src/TScopedTimer.cpp
src/Value.cpp
src/TServer.cpp
src/VehicleData.cpp
src/Network.cpp
src/Env.cpp
src/Settings.cpp
src/Profiling.cpp
src/ChronoWrapper.cpp
)
find_package(Lua REQUIRED)
@@ -83,7 +93,7 @@ set(PRJ_COMPILE_FEATURES cxx_std_20)
# set #defines (test enable/disable not included here)
set(PRJ_DEFINITIONS CPPHTTPLIB_OPENSSL_SUPPORT)
# add all libraries used by the project (WARNING: also set them in vcpkg.json!)
set(PRJ_LIBRARIES
set(PRJ_LIBRARIES
fmt::fmt
doctest::doctest
Threads::Threads
@@ -94,26 +104,21 @@ set(PRJ_LIBRARIES
httplib::httplib
libzip::zip
OpenSSL::SSL OpenSSL::Crypto
protocol
CURL::libcurl
${LUA_LIBRARIES}
zstd::libzstd_static
Boost::thread
Boost::json
glm::glm
)
# add dependency find_package calls and similar here
find_package(fmt CONFIG REQUIRED)
find_package(OpenSSL REQUIRED)
find_package(doctest CONFIG REQUIRED)
find_package(Boost REQUIRED COMPONENTS thread json)
find_package(Boost REQUIRED)
find_package(httplib CONFIG REQUIRED)
find_package(libzip CONFIG REQUIRED)
find_package(RapidJSON CONFIG REQUIRED)
find_package(sol2 CONFIG REQUIRED)
find_package(toml11 CONFIG REQUIRED)
find_package(zstd CONFIG REQUIRED)
find_package(glm CONFIG REQUIRED)
find_package(CURL CONFIG REQUIRED)
add_subdirectory("deps/toml11")
include_directories(include)
@@ -122,7 +127,7 @@ include(FindThreads)
### END SETTINGS ###
# DONT change anything beyond this point unless you've read the cmake bible and
# DONT change anything beyond this point unless you've read the cmake bible and
# swore on it not to bonk up the ci/cd pipelines with your changes.
####################
@@ -143,7 +148,9 @@ if(UNIX)
endif(UNIX)
if (WIN32)
add_compile_options("-D_WIN32_WINNT=0x0601")
add_compile_definitions(_WIN32_WINNT=0x0A00)
add_compile_options("/bigobj")
endif(WIN32)
@@ -160,7 +167,7 @@ set(PRJ_DEFINITIONS ${PRJ_DEFINITIONS}
)
# build commandline manually for funky windows flags to carry over without a custom toolchain file
add_library(commandline_static
add_library(commandline_static
deps/commandline/src/impls.h
deps/commandline/src/windows_impl.cpp
deps/commandline/src/linux_impl.cpp
@@ -173,6 +180,11 @@ add_library(commandline_static
deps/commandline/src/backends/BufferedBackend.cpp
deps/commandline/src/backends/BufferedBackend.h
)
# Ensure the commandline library uses C++11
set_target_properties(commandline_static PROPERTIES CXX_STANDARD 11 CXX_STANDARD_REQUIRED YES)
if (WIN32)
target_compile_definitions(commandline_static PRIVATE -DPLATFORM_WINDOWS=1)
else ()
@@ -197,6 +209,7 @@ target_compile_definitions(${PROJECT_NAME} PRIVATE ${PRJ_DEFINITIONS} ${PRJ_WARN
)
if(MSVC)
target_compile_options(${PROJECT_NAME} PUBLIC "/bigobj")
target_link_options(${PROJECT_NAME} PRIVATE "/SUBSYSTEM:CONSOLE")
endif(MSVC)
@@ -215,4 +228,3 @@ if(${PROJECT_NAME}_ENABLE_UNIT_TESTING)
target_link_options(${PROJECT_NAME}-tests PRIVATE "/SUBSYSTEM:CONSOLE")
endif(MSVC)
endif()
+22 -5
View File
@@ -53,11 +53,28 @@ Do **NOT** pull with merge. This is the default git behavior for `git pull`, but
The only acceptable merge commits are those which actually merge functionally different branches into each other, for example for merging one feature branch into another.
## Workflow
### Making an issue and fixing it
1. Create an issue detailing the feature or bug.
2. Assign a milestone to the issue, or wait for a maintainer to add a milestone to your issue.
3. Fork the repository and base your work on the branch mentioned in the milestone attached to your issue (e.g. `v3.0.0 (develop)` -> `develop`).
4. Program your feature or bug fix.
5. Open a PR that references the issue by number in the format: `#12345`.
6. Someone will review your PR and merge it, or ask for changes.
### Fixing an existing issue
1. Fork the repository and base your work on the branch mentioned in the milestone attached to your issue (e.g. `v3.0.0 (develop)` -> `develop`).
2. Program your feature or bug fix.
3. Open a PR that references the issue by number in the format: `#12345`.
4. Someone will review your PR and merge it, or ask for changes.
## Branches
### Which branch should I base my work on?
Each *feature* or *bug-fix* is implemented on a new Git branch, branched off of the branch it should be based on. The `master` branch is usually stable, so we don't do development on it. It is always a safe bet to branch off of `master`, but it may be more work to merge later. Branches to base your work on are usually branches like `rc-v3.3.0`, when the latest public version is `3.2.0`, for example. These can often be found in Pull-Requests on GitHub which are tagged `Release Candidate`.
- `minor`: Minor releases, like `v1.2.3` -> `v1.3.0` or `v1.2.3` -> `v1.2.4`.
- `develop`: Major releases, like `v1.2.3` -> `v2.0.0`, and larger feature/minor releases.
## Unit tests & CI/CD
@@ -91,8 +108,8 @@ A BeamMP-Developer must review your code in detail, and leave a review. If this
5. Run `git submodule update --init --recursive`.
6. Make a new branch for your feature or fix from the branch you are on. You can do this via `git checkout -b <branch name>`. See [this section on branches](#branches) for more info on branch naming.
7. Install all dependencies. Those are usually listed in the `README.md` in the branch you're in, or, more reliably, in one of the files in `.github/workflows` (if you can read `yaml`).
8. Run CMake to configure the project. You can find tutorials on this online. You will want to tell CMake to build with `CMAKE_BUILD_TYPE=Debug`, for example by passing it to CMake via the commandline switch `-DCMAKE_BUILD_TYPE=Debug`. You may also want to turn off sentry by setting `SENTRY_BACKEND=none` (for example via commandline switch `-DSENTRY_BACKEND=none`). An example invocation on Linux with GNU make would be
`cmake . -DCMAKE_BUILD_TYPE=Debug -DSENTRY_BACKEND=none` .
8. Run CMake to configure the project. You can find tutorials on this online. You will want to tell CMake to build with `CMAKE_BUILD_TYPE=Debug`, for example by passing it to CMake via the commandline switch `-DCMAKE_BUILD_TYPE=Debug`. An example invocation on Linux with GNU make would be
`cmake . -DCMAKE_BUILD_TYPE=Debug` .
9. Build the `BeamMP-Server` target to build the BeamMP-Server, or the `BeamMP-Server-tests` target to build the unit-tests (does not include a working server). In the example from 8. (on Linux), you could build with `make BeamMP-Server`, `make -j BeamMP-Server` or `cmake --build . --parallel --target BeamMP-Server` . Or, on Windows, (in Visual Studio), you would just press some big green "run" or "debug" button.
10. When making changes, refer to [this section on how to commit properly](#how-to-commit). Not following those guidelines will result in your changes being rejected, so please take a look.
11. Make sure to add Unit-tests with `doctest` if you build new stuff. You can find examples all over the latest version of the codebase (search for `TEST_CASE`).
+6 -5
View File
@@ -19,7 +19,7 @@ Feel free to ask any questions via the following channels:
These values are guesstimated and are subject to change with each release.
* RAM: 50+ MiB usable (not counting OS overhead)
* RAM: 30-100 MiB usable (not counting OS overhead)
* CPU: >1GHz, preferably multicore
* OS: Windows, Linux (theoretically any POSIX)
* GPU: None
@@ -38,7 +38,7 @@ If you need support with understanding the codebase, please write us in the Disc
## About Building from Source
We only allow building unmodified (original) source code for public use. `master` is considered **unstable** and we will not provide technical support if such a build doesn't work, so always build from a tag. You can checkout a tag with `git checkout tags/TAGNAME`, where `TAGNAME` is the tag, for example `v3.1.0`.
We only allow building unmodified (original) source code for public use. `master` is considered **unstable** and we will not provide technical support if such a build doesn't work, so always build from a tag. You can checkout a tag with `git checkout tags/TAGNAME`, where `TAGNAME` is the tag, for example `v3.4.1`. See [the tags](https://github.com/BeamMP/BeamMP-Server/tags) for possible versions/tags, as well as [the releases](https://github.com/BeamMP/BeamMP-Server/releases) to check which version is marked as a release/prerelease. We recommend using the [latest release](https://github.com/BeamMP/BeamMP-Server/releases/latest).
## Supported Operating Systems
@@ -66,9 +66,10 @@ You can build on **Windows, Linux** or other platforms by following these steps:
1. Check out the repository with git: `git clone --recursive https://github.com/BeamMP/BeamMP-Server`.
2. Go into the directory `cd BeamMP-Server`.
3. Run CMake `cmake -S . -B bin -DCMAKE_BUILD_TYPE=Release` - this can take a few minutes and may take a lot of disk space and bandwidth.
4. Build via `cmake --build bin --parallel --config Release -t BeamMP-Server`.
5. Your executable can be found in `bin/`.
3. Specify the server version to build by checking out a tag: `git checkout tags/v3.4.1` - [Possible versions/tags](https://github.com/BeamMP/BeamMP-Server/tags)
4. Run CMake `cmake -S . -B bin -DCMAKE_BUILD_TYPE=Release` - this can take a few minutes and may take a lot of disk space and bandwidth.
5. Build via `cmake --build bin --parallel --config Release -t BeamMP-Server`.
6. Your executable can be found in `bin/`.
When you make changes to the code, you only have to run step 4 again.
### Building for FreeBSD
+3 -3
View File
@@ -3,9 +3,9 @@
# https://github.com/lefticus/cppbestpractices/blob/master/02-Use_the_Tools_Available.md
# Courtesy of Jason Turner
# License here: https://github.com/cpp-best-practices/cppbestpractices/blob/master/LICENSE
#
#
# This version has been modified by the owners of the current respository.
# Modifications have mostly been marked with "modified" or similar, though this is not
# Modifications have mostly been marked with "modified" or similar, though this is not
# strictly required.
function(set_project_warnings project_name)
@@ -71,8 +71,8 @@ function(set_project_warnings project_name)
-Werror=write-strings
-Werror=strict-aliasing -fstrict-aliasing
-Werror=missing-declarations
-Werror=missing-field-initializers
-Werror=ctor-dtor-privacy
-Wno-missing-field-initializers
-Wswitch-default
-Werror=unused-result
-Werror=implicit-fallthrough
+1 -1
View File
@@ -2,7 +2,7 @@ find_package(Git)
if(${PROJECT_NAME}_CHECKOUT_GIT_SUBMODULES)
if(Git_FOUND)
message(STATUS "Git found, submodule update and init")
execute_process(COMMAND ${GIT_EXECUTABLE} submodule update --init --recursive
execute_process(COMMAND ${GIT_EXECUTABLE} submodule update --init
WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
RESULT_VARIABLE GIT_SUBMOD_RESULT)
if(NOT GIT_SUBMOD_RESULT EQUAL "0")
Submodule deps/BeamMP-Protocol deleted from 8eda5714c0
Vendored Submodule
+1
Submodule deps/toml11 added at f33ca743fb
+8
View File
@@ -0,0 +1,8 @@
#pragma once
#include <chrono>
#include <string>
namespace ChronoWrapper {
std::chrono::high_resolution_clock::duration TimeFromStringWithLiteral(const std::string& time_str);
}
+134
View File
@@ -0,0 +1,134 @@
// BeamMP, the BeamNG.drive multiplayer mod.
// Copyright (C) 2024 BeamMP Ltd., BeamMP team and contributors.
//
// BeamMP Ltd. can be contacted by electronic mail via contact@beammp.com.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published
// by the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
#pragma once
#include <chrono>
#include <memory>
#include <optional>
#include <queue>
#include <string>
#include <unordered_set>
#include <utility>
#include "BoostAliases.h"
#include "Common.h"
#include "Compat.h"
#include "VehicleData.h"
class TServer;
#ifdef BEAMMP_WINDOWS
// for socklen_t
#include <WS2tcpip.h>
#endif // WINDOWS
struct TConnection final {
ip::tcp::socket Socket;
ip::tcp::endpoint SockAddr;
};
class TClient final {
public:
using TSetOfVehicleData = std::vector<TVehicleData>;
struct TVehicleDataLockPair {
TSetOfVehicleData* VehicleData;
std::unique_lock<std::mutex> Lock;
};
TClient(TServer& Server, ip::tcp::socket&& Socket);
TClient(const TClient&) = delete;
~TClient();
TClient& operator=(const TClient&) = delete;
void AddNewCar(int Ident, const nlohmann::json& Data);
void SetCarData(int Ident, const nlohmann::json& Data);
void SetCarPosition(int Ident, const std::string& Data);
TVehicleDataLockPair GetAllCars();
void SetName(const std::string& Name) { mName = Name; }
void SetRoles(const std::string& Role) { mRole = Role; }
void SetIdentifier(const std::string& key, const std::string& value) { mIdentifiers[key] = value; }
nlohmann::json GetCarData(int Ident);
std::string GetCarPositionRaw(int Ident);
void SetUDPAddr(const ip::udp::endpoint& Addr) { mUDPAddress = Addr; }
void SetTCPSock(ip::tcp::socket&& CSock) { mSocket = std::move(CSock); }
void Disconnect(std::string_view Reason);
bool IsDisconnected() const { return !mSocket.is_open(); }
// locks
void DeleteCar(int Ident);
[[nodiscard]] const std::unordered_map<std::string, std::string>& GetIdentifiers() const { return mIdentifiers; }
[[nodiscard]] const ip::udp::endpoint& GetUDPAddr() const { return mUDPAddress; }
[[nodiscard]] ip::udp::endpoint& GetUDPAddr() { return mUDPAddress; }
[[nodiscard]] ip::tcp::socket& GetTCPSock() { return mSocket; }
[[nodiscard]] const ip::tcp::socket& GetTCPSock() const { return mSocket; }
[[nodiscard]] std::string GetRoles() const { return mRole; }
[[nodiscard]] std::string GetName() const { return mName; }
void SetUnicycleID(int ID) { mUnicycleID = ID; }
void SetID(int ID) { mID = ID; }
[[nodiscard]] int GetOpenCarID() const;
[[nodiscard]] int GetCarCount() const;
void ClearCars();
[[nodiscard]] int GetID() const { return mID; }
[[nodiscard]] int GetUnicycleID() const { return mUnicycleID; }
[[nodiscard]] bool IsUDPConnected() const { return mIsUDPConnected; }
[[nodiscard]] bool IsSynced() const { return mIsSynced; }
[[nodiscard]] bool IsSyncing() const { return mIsSyncing; }
[[nodiscard]] bool IsGuest() const { return mIsGuest; }
void SetIsGuest(bool NewIsGuest) { mIsGuest = NewIsGuest; }
void SetIsSynced(bool NewIsSynced) { mIsSynced = NewIsSynced; }
void SetIsSyncing(bool NewIsSyncing) { mIsSyncing = NewIsSyncing; }
void EnqueuePacket(const std::vector<uint8_t>& Packet);
[[nodiscard]] std::queue<std::vector<uint8_t>>& MissedPacketQueue() { return mPacketsSync; }
[[nodiscard]] const std::queue<std::vector<uint8_t>>& MissedPacketQueue() const { return mPacketsSync; }
[[nodiscard]] size_t MissedPacketQueueSize() const { return mPacketsSync.size(); }
[[nodiscard]] std::mutex& MissedPacketQueueMutex() const { return mMissedPacketsMutex; }
void SetIsUDPConnected(bool NewIsConnected) { mIsUDPConnected = NewIsConnected; }
[[nodiscard]] TServer& Server() const;
void UpdatePingTime();
int SecondsSinceLastPing();
void SetMagic(std::vector<uint8_t> magic) { mMagic = std::move(magic); }
[[nodiscard]] const std::vector<uint8_t>& GetMagic() const { return mMagic; }
private:
void InsertVehicle(int ID, const std::string& Data);
TServer& mServer;
bool mIsUDPConnected = false;
bool mIsSynced = false;
bool mIsSyncing = false;
mutable std::mutex mMissedPacketsMutex;
std::queue<std::vector<uint8_t>> mPacketsSync;
std::unordered_map<std::string, std::string> mIdentifiers;
bool mIsGuest = false;
mutable std::mutex mVehicleDataMutex;
mutable std::mutex mVehiclePositionMutex;
TSetOfVehicleData mVehicleData;
SparseArray<std::string> mVehiclePosition;
std::string mName = "Unknown Client";
ip::tcp::socket mSocket;
ip::udp::endpoint mUDPAddress {};
int mUnicycleID = -1;
std::string mRole;
std::string mDID;
int mID = -1;
std::chrono::time_point<std::chrono::high_resolution_clock> mLastPingTime = std::chrono::high_resolution_clock::now();
std::vector<uint8_t> mMagic;
};
std::optional<std::weak_ptr<TClient>> GetClient(class TServer& Server, int ID);
+35 -56
View File
@@ -18,20 +18,18 @@
#pragma once
#include <algorithm>
#include <array>
#include <atomic>
#include <cstring>
#include <deque>
#include <filesystem>
#include <fmt/chrono.h>
#include <fmt/format.h>
#include <fmt/ostream.h>
#include <fmt/ranges.h>
#include <fmt/std.h>
#include <functional>
#include <memory>
#include <mutex>
#include <shared_mutex>
#include <span>
#include <sstream>
#include <unordered_map>
#include <zlib.h>
@@ -40,6 +38,7 @@
#include <filesystem>
namespace fs = std::filesystem;
#include "Settings.h"
#include "TConsole.h"
struct Version {
@@ -62,32 +61,6 @@ using SparseArray = std::unordered_map<size_t, T>;
class Application final {
public:
// types
struct TSettings {
std::string ServerName { "BeamMP Server" };
std::string ServerDesc { "BeamMP Default Description" };
std::string ServerTags { "Freeroam" };
std::string Resource { "Resources" };
std::string MapName { "/levels/gridmap_v2/info.json" };
std::string Key {};
std::string Password {};
std::string SSLKeyPath { "./.ssl/HttpServer/key.pem" };
std::string SSLCertPath { "./.ssl/HttpServer/cert.pem" };
bool HTTPServerEnabled { false };
int MaxPlayers { 8 };
bool Private { true };
int MaxCars { 1 };
bool DebugModeEnabled { false };
int Port { 30814 };
std::string CustomIP {};
bool LogChat { true };
bool SendErrors { true };
bool SendErrorsMessageEnabled { true };
int HTTPServerPort { 8080 };
std::string HTTPServerIP { "127.0.0.1" };
bool HTTPServerUseSSL { false };
bool HideUpdateMessages { false };
[[nodiscard]] bool HasCustomIP() const { return !CustomIP.empty(); }
};
using TShutdownHandler = std::function<void()>;
@@ -101,21 +74,21 @@ public:
static TConsole& Console() { return mConsole; }
static std::string ServerVersionString();
static const Version& ServerVersion() { return mVersion; }
static uint8_t ClientMajorVersion() { return 2; }
static Version ClientMinimumVersion() { return Version { 2, 7, 0 }; }
static std::string PPS() { return mPPS; }
static void SetPPS(const std::string& NewPPS) { mPPS = NewPPS; }
static TSettings Settings;
static inline struct Settings Settings { };
static std::vector<std::string> GetBackendUrlsInOrder() {
return {
"backend.beammp.com",
"backup1.beammp.com",
"backup2.beammp.com"
"https://backend.beammp.com",
};
}
static std::string GetBackendUrlForAuth() { return "auth.beammp.com"; }
static std::string GetServerCheckUrl() { return "https://check.beammp.com"; }
static std::string GetBackendUrlForAuth() { return "https://auth.beammp.com"; }
static std::string GetBackendUrlForSocketIO() { return "https://backend.beammp.com"; }
static void CheckForUpdates();
static std::array<uint8_t, 3> VersionStrToInts(const std::string& str);
@@ -156,10 +129,11 @@ private:
static inline std::mutex mShutdownHandlersMutex {};
static inline std::deque<TShutdownHandler> mShutdownHandlers {};
static inline Version mVersion { 3, 2, 2 };
static inline Version mVersion { 3, 9, 1 };
};
void SplitString(std::string const& str, const char delim, std::vector<std::string>& out);
std::string LowerString(std::string str);
std::string ThreadName(bool DebugModeOverride = false);
void RegisterThread(const std::string& str);
@@ -168,7 +142,6 @@ void RegisterThread(const std::string& str);
#define KB 1024llu
#define MB (KB * 1024llu)
#define GB (MB * 1024llu)
#define SSU_UNRAW SECRET_SENTRY_URL
#define _file_basename std::filesystem::path(__FILE__).filename().string()
#define _line std::to_string(__LINE__)
@@ -191,13 +164,13 @@ void RegisterThread(const std::string& str);
#else
#define _function_name std::string(__func__)
#endif
#ifndef NDEBUG
#define DEBUG
#endif
#if defined(DEBUG)
// if this is defined, we will show the full function signature infront of
// each info/debug/warn... call instead of the 'filename:line' format.
#if defined(BMP_FULL_FUNCTION_NAMES)
@@ -207,7 +180,7 @@ void RegisterThread(const std::string& str);
#endif
#endif // defined(DEBUG)
#define beammp_warn(x) Application::Console().Write(_this_location + std::string("[WARN] ") + (x))
#define beammp_info(x) Application::Console().Write(_this_location + std::string("[INFO] ") + (x))
#define beammp_error(x) \
@@ -218,30 +191,24 @@ void RegisterThread(const std::string& str);
do { \
Application::Console().Write(_this_location + std::string("[LUA ERROR] ") + (x)); \
} while (false)
#define beammp_lua_log(level, plugin, x) \
do { \
Application::Console().Write(_this_location + fmt::format("[{}] [{}] ", plugin, level) + (x)); \
} while (false)
#define beammp_lua_warn(x) \
do { \
Application::Console().Write(_this_location + std::string("[LUA WARN] ") + (x)); \
} while (false)
#define beammp_lua_info(x) \
do { \
Application::Console().Write(_this_location + std::string("[LUA INFO] ") + (x)); \
} while (false)
#define beammp_lua_debug(x) \
do { \
if (Application::Settings.DebugModeEnabled) { \
Application::Console().Write(_this_location + std::string("[LUA DEBUG] ") + (x)); \
} \
} while (false)
#define luaprint(x) Application::Console().Write(_this_location + std::string("[LUA] ") + (x))
#define beammp_debug(x) \
do { \
if (Application::Settings.DebugModeEnabled) { \
if (Application::Settings.getAsBool(Settings::Key::General_Debug)) { \
Application::Console().Write(_this_location + std::string("[DEBUG] ") + (x)); \
} \
} while (false)
#define beammp_event(x) \
do { \
if (Application::Settings.DebugModeEnabled) { \
if (Application::Settings.getAsBool(Settings::Key::General_Debug)) { \
Application::Console().Write(_this_location + std::string("[EVENT] ") + (x)); \
} \
} while (false)
@@ -249,19 +216,21 @@ void RegisterThread(const std::string& str);
#if defined(DEBUG)
#define beammp_trace(x) \
do { \
if (Application::Settings.DebugModeEnabled) { \
if (Application::Settings.getAsBool(Settings::Key::General_Debug)) { \
Application::Console().Write(_this_location + std::string("[TRACE] ") + (x)); \
} \
} while (false)
#else
#define beammp_trace(x)
#endif // defined(DEBUG)
#define beammp_errorf(...) beammp_error(fmt::format(__VA_ARGS__))
#define beammp_infof(...) beammp_info(fmt::format(__VA_ARGS__))
#define beammp_debugf(...) beammp_debug(fmt::format(__VA_ARGS__))
#define beammp_warnf(...) beammp_warn(fmt::format(__VA_ARGS__))
#define beammp_tracef(...) beammp_trace(fmt::format(__VA_ARGS__))
#define beammp_lua_errorf(...) beammp_lua_error(fmt::format(__VA_ARGS__))
#define beammp_lua_warnf(...) beammp_lua_warn(fmt::format(__VA_ARGS__))
#else // DOCTEST_CONFIG_DISABLE
@@ -281,6 +250,7 @@ void RegisterThread(const std::string& str);
#define beammp_tracef(...) beammp_trace(fmt::format(__VA_ARGS__))
#define beammp_lua_errorf(...) beammp_lua_error(fmt::format(__VA_ARGS__))
#define beammp_lua_warnf(...) beammp_lua_warn(fmt::format(__VA_ARGS__))
#define beammp_lua_log(level, plugin, x) /* x */
#endif // DOCTEST_CONFIG_DISABLE
@@ -295,5 +265,14 @@ void RegisterThread(const std::string& str);
void LogChatMessage(const std::string& name, int id, const std::string& msg);
std::vector<uint8_t> Comp(std::span<const uint8_t> input);
std::vector<uint8_t> DeComp(std::span<const uint8_t> input);
std::string GetPlatformAgnosticErrorString();
#define S_DSN SU_RAW
class InvalidDataError : std::runtime_error {
public:
InvalidDataError() : std::runtime_error("Invalid data") {
}
};
+4 -3
View File
@@ -32,6 +32,7 @@
#include <thread>
#include "Common.h"
#include "Compat.h"
static const char* const ANSI_RESET = "\u001b[0m";
@@ -56,7 +57,7 @@ static const char* const ANSI_WHITE_BOLD = "\u001b[37;1m";
static const char* const ANSI_BOLD = "\u001b[1m";
static const char* const ANSI_UNDERLINE = "\u001b[4m";
#ifdef DEBUG
#if defined(DEBUG) && !defined(BEAMMP_FREEBSD)
#include <iostream>
inline void _assert([[maybe_unused]] const char* file, [[maybe_unused]] const char* function, [[maybe_unused]] unsigned line,
[[maybe_unused]] const char* condition_string, [[maybe_unused]] bool result) {
@@ -81,8 +82,8 @@ inline void _assert([[maybe_unused]] const char* file, [[maybe_unused]] const ch
beammp_errorf("Assertion failed in '{}:{}': {}.", __func__, _line, #cond); \
} \
} while (false)
#define beammp_assert_not_reachable() \
do { \
#define beammp_assert_not_reachable() \
do { \
beammp_errorf("Assertion failed in '{}:{}': Unreachable code reached. This may result in a crash or undefined state of the program.", __func__, _line); \
} while (false)
#endif // DEBUG
+3
View File
@@ -25,6 +25,9 @@ namespace Env {
enum class Key {
// provider settings
PROVIDER_UPDATE_MESSAGE,
PROVIDER_DISABLE_CONFIG,
PROVIDER_PORT_ENV,
PROVIDER_IP_ENV
};
std::optional<std::string> Get(Key key);
-120
View File
@@ -1,120 +0,0 @@
#pragma once
#include <fmt/core.h>
#include <optional>
#include <string>
/// The Error class represents an error or the absence of an
/// error. It behaves like a bool, depending on context.
///
/// The idea is to use this class to pass around errors, together with
/// [[nodiscard]], in order to make errors displayable for the users and
/// to give errors some context. The only way to construct an error is to come
/// up with an error message with this class, so this is an attempt to enforce
/// this.
///
/// A default constructed Error means "no error" / "success", while
/// the only available non-default constructor is one which takes any format.
/// For example:
///
/// \code{.cpp}
/// Error myfunc() {
/// if (ok) {
/// return {}; // no error
/// } else {
/// return Error("Something went wrong: {}", 42); // error
/// }
/// }
///
/// // ... handling:
///
/// Error err = myfunc();
/// if (err) {
/// // error occurred
/// l::error("Error running myfunc: {}", err.error);
/// } else {
/// // ok
/// }
/// \endcode
struct Error {
/// Constructs a "non-error" / empty error, which is not considered
/// to be an error. Use this as the "no error occurred" return value.
Error() = default;
/// Constructs an error with a message. Accepts fmt::format() arguments.
///
/// Example:
///
/// \code{.cpp}
/// // format with fmt (automatically), all arguments are forwarded to fmt::format
/// return Error("failed to open '{}': {}", file, error);
/// // or just as a constexpr string
/// return Error("failed to open file");
/// \endcode
template<typename... Args>
Error(fmt::format_string<Args...> s, Args&&... args)
: is_error(true)
, error(fmt::format(s, std::forward<Args>(args)...)) { }
/// Whether this error represents an error (true) or success (false).
/// Use operator bool() instead of reading this if possible.
bool is_error { false };
/// The error message. Is a valid string even if is_error is false, but will
/// be "Success".
std::string error { "Success" };
/// Implicit conversion to boolean.
/// True if this Error contains an error, false if not.
operator bool() const { return is_error; }
};
// TODO: Add docs
template<typename T>
struct Result {
/// Constructs an error-value result.
/// Currently, you may have to force this by passing a second
/// empty string argument.
template<typename... Args>
Result(fmt::format_string<Args...> s, Args&&... args)
: is_error(true)
, error(fmt::format(s, std::forward<Args>(args)...)) { }
/// Constructs a value-result via an explicit type.
template<typename S>
Result(S&& value)
: result(std::move(value)) {
}
/// Constructs a value-result via an explicit type.
template<typename S>
explicit Result(const S& value)
: result(value) {
}
/// Constructs a value-result via an implicit conversion.
Result(T&& value)
: result(std::move(value)) {
}
/// Constructs a value-result via an implicit conversion.
explicit Result(const T& value)
: result(value) {
}
/// Converts to bool in context. If it has an error, its considered "not a result",
/// so it returns true only if a value is contained.
operator bool() const { return !is_error; }
/// Accesses the value contained by moving it out.
T&& move() { return std::move(result.value()); }
/// Accesses the value contained by const reference.
const T& value() const { return result.value(); }
/// Holds the optional result value. On error, is nullopt.
std::optional<T> result;
/// Whether this result holds an error.
bool is_error { false };
/// Error message.
std::string error { "Success" };
};
-92
View File
@@ -1,92 +0,0 @@
#pragma once
/// @file
/// This file holds the FileWatcher interface.
#include <boost/asio/deadline_timer.hpp>
#include <boost/asio/io_context.hpp>
#include <boost/date_time/posix_time/posix_time_duration.hpp>
#include <boost/signals2.hpp>
#include <boost/system/detail/error_code.hpp>
#include <boost/thread/scoped_thread.hpp>
#include <boost/thread/synchronized_value.hpp>
#include <filesystem>
#include <functional>
#include <unordered_map>
#include <unordered_set>
/// The FileWatcher class watches a directory or a file for changes,
/// and then notifies the caller through a signal.
///
/// This is a pretty convoluted implementation, and you may find it difficult
/// to read. This is not intentional, but simplifying this would
/// cost more time than to write this excuse.
///
/// It operates as follows:
///
/// A boost::asio::deadline_timer is waited on asynchronously.
/// Once expired, this timer calls FileWatcher::on_tick.
/// That function then loops through all registered files and directories,
/// taking great care to follow symlinks, and tries to find a file which has changed.
/// It determines this by storing the last known modification time.
/// Once a file is found which has a new modification time, the FileWatcher::sig_file_changed
/// signal is fired, and all connected slots must take care to handle the signal.
class FileWatcher {
public:
/// Constructs the FileWatcher to watch the given files every few seconds, as
/// specified by the seconds argument.
FileWatcher(unsigned seconds);
/// Stops the thread via m_shutdown.
~FileWatcher();
/// Add a file to watch. If this file changes, FileWatcher::sig_file_changed is triggered
/// with the path to the file.
void watch_file(const std::filesystem::path& path);
/// Add a directory to watch. If any file in the directory, or any subdirectories, change,
/// FileWatcher::sig_file_changed is triggered.
void watch_files_in(const std::filesystem::path& dir);
boost::signals2::signal<void(const std::filesystem::path&)> sig_file_changed {};
private:
/// Entry point for the timer thread.
void thread_main();
/// Called every time the timer runs out, watches for file changes, then starts
/// a new timer.
void on_tick(const boost::system::error_code&);
/// Checks files for changes, calls FileWatcher::sig_file_changed on change.
void check_files();
/// Checks directories for files which changed, calls FileWatcher::sig_file_changed on change.
void check_directories();
/// Checks a single file for change.
void check_file(const std::filesystem::path& file);
/// Interval in seconds for the timer. Needed to be able to restart the timer over and over.
boost::synchronized_value<boost::posix_time::seconds> m_seconds;
/// If set, the thread handling the file watching will shut down. Set in the destructor.
boost::synchronized_value<bool> m_shutdown { false };
/// Io context handles the scheduling of timers on the thread.
boost::asio::io_context m_io {};
/// Holds all files that are to be checked.
///
/// It uses a boost::hash<> because in the original C++17
/// standard, std::hash of a filesystem path was not defined, and as such
/// some implementations still don't have it.
/// See https://cplusplus.github.io/LWG/issue3657
boost::synchronized_value<std::unordered_set<std::filesystem::path, boost::hash<std::filesystem::path>>> m_files {};
/// Holds all the directories that are to be searched for files to be checked.
///
/// See FileWatcher::m_files for an explanation for the boost::hash.
boost::synchronized_value<std::unordered_set<std::filesystem::path, boost::hash<std::filesystem::path>>> m_dirs {};
/// Holds the last known modification times of all found files.
std::unordered_map<std::filesystem::path, std::filesystem::file_time_type, boost::hash<std::filesystem::path>> m_file_mod_times {};
/// Timer used to time the checks. Restarted every FileWatcher::m_seconds seconds.
boost::synchronized_value<boost::asio::deadline_timer> m_timer;
/// Work guard helps the io_context "sleep" while there is no work to be done - must be reset in the
/// destructor in order to not cause work to be thrown away (though in this case we probably don't care).
boost::asio::executor_work_guard<boost::asio::io_context::executor_type> m_work_guard = boost::asio::make_work_guard(m_io);
/// Thread on which all watching and timing work runs.
boost::scoped_thread<> m_thread;
};
-22
View File
@@ -1,22 +0,0 @@
#pragma once
/// @file
/// HashMap holds hash map implementations and typedefs.
///
/// The idea is that we can easily swap out the implementation
/// in case there is a performance or memory usage concern.
#include <boost/container/flat_map.hpp>
#include <boost/thread/synchronized_value.hpp>
/// A hash map to be used for any kind of small number of key-value pairs.
/// Iterators and pointers may be invalidated on modification.
template<typename K, typename V>
using HashMap = boost::container::flat_map<K, V>;
/// A synchronized hash map is a hash map in which each
/// access is thread-safe. In this case, this is achieved by locking
/// each access with a mutex (which often ends up being a futex in the implementation).
template<typename K, typename V>
using SynchronizedHashMap = boost::synchronized_value<boost::container::flat_map<K, V>>;
+3 -2
View File
@@ -23,6 +23,7 @@
#include <filesystem>
#include <string>
#include <unordered_map>
#include <curl/curl.h>
#if defined(BEAMMP_LINUX)
#pragma GCC diagnostic push
@@ -38,8 +39,8 @@
namespace fs = std::filesystem;
namespace Http {
std::string GET(const std::string& host, int port, const std::string& target, unsigned int* status = nullptr);
std::string POST(const std::string& host, int port, const std::string& target, const std::string& body, const std::string& ContentType, unsigned int* status = nullptr, const httplib::Headers& headers = {});
std::string GET(const std::string& url, unsigned int* status = nullptr);
std::string POST(const std::string& url, const std::string& body, const std::string& ContentType, unsigned int* status = nullptr, const std::map<std::string, std::string>& headers = {});
namespace Status {
std::string ToString(int code);
}
+5 -4
View File
@@ -18,7 +18,6 @@
#pragma once
#include <boost/thread/scoped_thread.hpp>
#include <thread>
// pure virtual class to be inherited from by classes which intend to be threaded
@@ -28,14 +27,16 @@ public:
// invokes operator() on this object
: mThread() { }
virtual ~IThreaded() noexcept {
mThread.interrupt();
if (mThread.joinable()) {
mThread.join();
}
}
virtual void Start() final {
mThread = boost::scoped_thread<>([this] { (*this)(); });
mThread = std::thread([this] { (*this)(); });
}
virtual void operator()() = 0;
protected:
boost::scoped_thread<> mThread {};
std::thread mThread;
};
+2 -2
View File
@@ -17,7 +17,7 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>.
#pragma once
#include "rapidjson/stringbuffer.h"
#include "rapidjson/prettywriter.h"
#include "rapidjson/document.h"
#include "rapidjson/prettywriter.h"
#include "rapidjson/stringbuffer.h"
#include "rapidjson/writer.h"
+13 -15
View File
@@ -18,33 +18,33 @@
#pragma once
#include <sol/sol.hpp>
#include <string>
#include "TLuaEngine.h"
#include <tuple>
#include <utility>
namespace LuaAPI {
int PanicHandler(lua_State* State);
std::string LuaToString(const sol::object Value, size_t Indent = 1, bool QuoteStrings = false);
void Print(sol::variadic_args);
namespace MP {
extern TLuaEngine* Engine;
std::string GetOSName();
std::tuple<int, int, int> GetServerVersion();
std::pair<bool, std::string> TriggerClientEvent(int PlayerID, const std::string& EventName, const sol::object& Data);
std::pair<bool, std::string> TriggerClientEventJson(int PlayerID, const std::string& EventName, const sol::table& Data);
size_t GetPlayerCount();
inline size_t GetPlayerCount() { return Engine->Server().ClientCount(); }
std::pair<bool, std::string> DropPlayer(int ID, std::optional<std::string> MaybeReason);
std::pair<bool, std::string> SendChatMessage(int ID, const std::string& Message);
std::pair<bool, std::string> SendNotification(int ID, const std::string& Message, const std::string& Icon, const std::string& Category);
std::pair<bool, std::string> ConfirmationDialog(int ID, const std::string& Title, const std::string& Body, const sol::table& buttons, const std::string& InteractionID, const bool& warning = false, const bool& reportToServer = true, const bool& reportToExtensions = true);
std::pair<bool, std::string> RemoveVehicle(int PlayerID, int VehicleID);
void Set(int ConfigID, sol::object NewValue);
TLuaValue Get(int ConfigID);
bool IsPlayerGuest(int ID);
bool IsPlayerConnected(int ID);
/// Returns the current time in millisecond accuracy.
size_t GetTimeMS();
/// Returns the current time in seconds, with millisecond accuracy (w/ decimal point).
double GetTimeS();
}
namespace Util {
std::string JsonEncode(const sol::object& object);
sol::table JsonDecode(sol::this_state s, const std::string& string);
void Sleep(size_t Ms);
void PrintRaw(sol::variadic_args);
std::string JsonEncode(const sol::table& object);
std::string JsonDiff(const std::string& a, const std::string& b);
std::string JsonDiffApply(const std::string& data, const std::string& patch);
std::string JsonPrettify(const std::string& json);
@@ -65,7 +65,5 @@ namespace FS {
bool IsDirectory(const std::string& Path);
bool IsFile(const std::string& Path);
std::string ConcatPaths(sol::variadic_args Args);
sol::table ListFiles(sol::this_state s, const std::string& path);
sol::table ListDirectories(sol::this_state s, const std::string& path);
}
}
-144
View File
@@ -1,144 +0,0 @@
#pragma once
#include "Common.h"
#include "FileWatcher.h"
#include "HashMap.h"
#include "Plugin.h"
#include <boost/asio/deadline_timer.hpp>
#include <boost/asio/io_service.hpp>
#include <boost/asio/post.hpp>
#include <boost/date_time/posix_time/posix_time_duration.hpp>
#include <boost/signals2.hpp>
#include <boost/thread/scoped_thread.hpp>
#include <boost/thread/synchronized_value.hpp>
#include <sol/forward.hpp>
#include <sol/variadic_args.hpp>
#include <spdlog/logger.h>
#include <sol/sol.hpp>
struct Timer {
Timer(boost::asio::deadline_timer&& timer_, long interval_)
: timer(std::move(timer_))
, interval(interval_) { }
boost::asio::deadline_timer timer;
boost::posix_time::milliseconds interval;
};
static constexpr const char* BEAMMP_MEMORY_STATE = ":console:";
class LuaPlugin : public Plugin {
public:
/// Declare a new plugin with the path.
/// Loading of any files only happens on LuaPlugin::initialize().
LuaPlugin(const std::string& path);
/// Shuts down lua thread, may hang if there is still work to be done.
~LuaPlugin();
template <typename FnT>
void register_function(const std::string& table, const std::string& identifier, const FnT& func) {
boost::asio::post(m_io, [this, table, identifier, func] {
if (!m_state.globals()[table].valid()) {
m_state.globals().create_named(table);
}
if (m_state.globals()[table][identifier].valid()) {
beammp_errorf("Global '{}.{}' already exists and could not be injected as function.", table, identifier);
} else {
m_state.globals()[table][identifier] = func;
}
});
}
/// Dangerous: Only use this on a special memory state.
/// Results are *printed to stdout*.
void run_raw_lua(const std::string& raw);
private:
/// Initializes the error handlers for panic and exceptions.
Error initialize_error_handlers();
/// Initializes / loads base packages and libraries.
Error initialize_libraries();
/// Loads main file of the plugin.
Error load_files();
/// Overrides functions such as `print()`
Error initialize_overrides();
/// Fixes lua's package.path and package.cpath to understand our own file structure better.
Error fix_lua_paths();
/// Loads an extension. Call this from the lua thread.
/// This function cannot fail, as it reports errors to the user.
void load_extension(const std::filesystem::path& file, const std::string& ext_name);
/// Loads all extension from the folder, using the base as a prefix.
/// This function is recursive.
/// Returns the amount of extensions found. This function cannot fail.
size_t load_extensions(const std::filesystem::path& extensions_folder, const std::string& base = "");
/// Entry point for the lua plugin's thread.
void thread_main();
// Plugin interface
public:
/// Initializes the Lua Plugin, loads file(s), starts executing code.
virtual Error initialize() override;
// TODO cleanup
virtual Error cleanup() override;
// TODO reload
virtual Error reload() override;
/// Name of this lua plugin (the base name of the folder).
virtual std::string name() const override;
/// Path to the folder containing this lua plugin.
virtual std::filesystem::path path() const override;
/// Dispatches the event to the thread which runs all lua.
virtual std::shared_future<std::vector<Value>> handle_event(const std::string& event_name, const std::shared_ptr<Value>& args) override;
/// Returns the memory usage of this thread, updated at the slowest every 5 seconds.
virtual size_t memory_usage() const override;
private:
/// Path to the plugin's root folder.
std::filesystem::path m_path;
/// Thread where all lua work must happen. Started within the constructor but is blocked until LuaPlugin::initialize is called
boost::scoped_thread<> m_thread;
/// This asio context schedules all tasks. It's run in the m_thread thread.
boost::asio::io_context m_io;
/// Event handlers which are legacy-style (by name)
HashMap<std::string, std::vector<std::string>> m_event_handlers_named {};
/// Event handlers which are functions (v4 style)
HashMap<std::string, std::vector<sol::protected_function>> m_event_handlers {};
/// Main (and usually only) lua state of this plugin.
/// ONLY access this from the m_thread thread.
sol::state m_state;
/// Whether the lua thread should shutdown. Triggered by the LuaPlugin::~LuaPlugin dtor.
boost::synchronized_value<bool> m_shutdown { false };
/// Current memory usage. Cached to avoid having to synchronize access to the lua state.
boost::synchronized_value<size_t> m_memory { 0 };
/// Hash map of all event handlers in this state.
// HashMap<std::string, sol::protected_function> m_event_handlers {};
SynchronizedHashMap<std::string, std::filesystem::path> m_known_extensions {};
boost::asio::executor_work_guard<boost::asio::io_context::executor_type> m_work_guard = boost::asio::make_work_guard(m_io);
/// Iteration options to be used whenever iterating over a directory in this class.
static inline auto s_directory_iteration_options = std::filesystem::directory_options::follow_directory_symlink | std::filesystem::directory_options::skip_permission_denied;
FileWatcher m_extensions_watcher { 2 };
boost::signals2::connection m_extensions_watch_conn;
std::vector<std::shared_ptr<Timer>> m_timers {};
std::shared_ptr<Timer> make_timer(size_t ms);
void cancel_timer(const std::shared_ptr<Timer>& timer);
// Lua API
/// Override for lua's base.print().
/// Dumps tables, arrays, etc. properly.
void l_print(const sol::variadic_args&);
std::shared_ptr<Timer> l_mp_schedule_call_repeat(size_t ms, const sol::function& fn, sol::variadic_args args);
void l_mp_schedule_call_helper(const boost::system::error_code& err, std::shared_ptr<Timer> timer, const sol::function& fn, std::shared_ptr<ValueTuple> args);
void l_mp_schedule_call_once(size_t ms, const sol::function& fn, sol::variadic_args args);
std::string print_impl(const std::vector<sol::object>&);
};
-250
View File
@@ -1,250 +0,0 @@
#pragma once
#include "Common.h"
#include "Packet.h"
#include "State.h"
#include "Sync.h"
#include "Transport.h"
#include "Util.h"
#include <boost/asio.hpp>
#include <boost/asio/execution_context.hpp>
#include <boost/asio/thread_pool.hpp>
#include <boost/thread/scoped_thread.hpp>
#include <boost/thread/synchronized_value.hpp>
#include <cstdint>
#include <filesystem>
#include <glm/detail/qualifier.hpp>
#include <glm/vec3.hpp>
#include <glm/vec4.hpp>
#include <memory>
#include <mutex>
#include <nlohmann/json.hpp>
#include <optional>
#include <unordered_map>
#include <vector>
using ClientID = uint32_t;
using VehicleID = uint16_t;
using namespace boost::asio;
struct Client : std::enable_shared_from_this<Client> {
using StrandPtr = std::shared_ptr<boost::asio::strand<ip::tcp::socket::executor_type>>;
using Ptr = std::shared_ptr<Client>;
ClientID id;
Sync<bmp::State> state { bmp::State::None };
Sync<std::string> name;
Sync<std::string> role;
Sync<bool> is_guest;
Sync<std::unordered_map<std::string /* identifier */, std::string /* value */>> identifiers;
/// Writes the packet to the TCP stream. Blocks all other writes.
void tcp_write(bmp::Packet& packet);
/// Writes the specified to the TCP stream without a header or any metadata - use in
/// conjunction with something else. Blocks other writes.
void tcp_write_file_raw(const std::filesystem::path& path);
/// Ensures no client is ever created as a non-shared-ptr, so that enable_shared_from_this works.
static Client::Ptr make_ptr(ClientID new_id, class Network& network, ip::tcp::socket&& tcp_socket);
~Client();
ip::tcp::socket& tcp_socket() { return m_tcp_socket; }
void start_tcp();
/// Used to associate the udp socket with this client.
/// This isn't very secure and still allows spoofing of the UDP connection (technically),
/// but better than simply using the ID like the old protocol.
const uint64_t udp_magic;
const ip::udp::endpoint& udp_endpoint() const { return m_udp_endpoint; }
void set_udp_endpoint(const ip::udp::endpoint& ep) { m_udp_endpoint = ep; }
private:
/// Ctor must be private to ensure all clients are constructed as shared_ptr to enable_shared_from_this.
Client(ClientID id, class Network& network, ip::tcp::socket&& tcp_socket);
/// Call this when the client seems to have timed out. Will send a ping and set a flag.
/// Returns true if try-again, false if the connection was closed.
[[nodiscard]] bool handle_timeout();
bool m_timed_out { false };
/// Timeout used for typical tcp reads.
boost::posix_time::milliseconds m_read_timeout { 5000 };
/// Timeout used for typical tcp writes. Specified in milliseconds per byte.
/// For example, 10 mbit/s works out to 1250 B/ms, so a value of 1250 here would
/// cause clients with >10 mbit/s download speed to usually not time out.
/// This is done because a write is considered completed when all data is written,
/// and worst-case this could mean that we're limited by their download speed.
/// We're setting it to 50, which will drop clients who are below a download speed + ping
/// combination of 0.4 mbit/s.
double m_write_byte_timeout { 0.01 };
/// Timeout used for mod download tcp writes.
/// This is typically orders of magnitude larger
/// to allow for slow downloads.
boost::posix_time::milliseconds m_download_write_timeout { 60000 };
std::mutex m_tcp_read_mtx;
std::mutex m_tcp_write_mtx;
std::mutex m_udp_read_mtx;
ip::tcp::socket m_tcp_socket;
boost::scoped_thread<> m_tcp_thread;
std::vector<uint8_t> m_header { bmp::Header::SERIALIZED_SIZE };
bmp::Packet m_packet {};
ip::udp::endpoint m_udp_endpoint;
class Network& m_network;
StrandPtr m_tcp_strand;
};
struct Vehicle {
using Ptr = std::shared_ptr<Vehicle>;
Sync<ClientID> owner;
Sync<std::vector<uint8_t>> data;
Vehicle(std::span<uint8_t> raw_data)
: data(std::vector<uint8_t>(raw_data.begin(), raw_data.end())) {
reset_status(data.get());
}
/// Resets all status fields to zero and reads any statuses present in the data into the fields.
void reset_status(std::span<const uint8_t> status_data);
struct Status {
glm::vec3 rvel {};
glm::vec4 rot {};
glm::vec3 vel {};
glm::vec3 pos {};
float time {};
};
Status get_status();
void update_status(std::span<const uint8_t> raw_packet);
const std::vector<uint8_t>& get_raw_status() const { return m_status_data; }
private:
std::recursive_mutex m_mtx;
/// Holds pos, rvel, vel, etc. raw, updated every time
/// such a packet arrives.
std::vector<uint8_t> m_status_data;
/// Parses the status_data on request sets needs_refresh = false.
void refresh_cache(std::unique_lock<std::recursive_mutex>& lock);
bool m_needs_refresh = false;
glm::vec3 m_rvel {};
glm::vec4 m_rot {};
glm::vec3 m_vel {};
glm::vec3 m_pos {};
float m_time {};
};
class Network {
public:
Network();
~Network();
friend Client;
void disconnect(ClientID id, const std::string& msg);
void send_to(ClientID id, bmp::Packet& packet);
/// Returns a map of <id, client> containing only clients which are
/// fully connected, i.e. who have mods downloaded and everything spawned in.
/// If you're unsure which to use, use this one.
std::unordered_map<ClientID, Client::Ptr> playing_clients() const;
/// Returns a map of <id, client> containing only clients who are authenticated.
std::unordered_map<ClientID, Client::Ptr> authenticated_clients() const;
/// Returns all clients, including non-authenticated clients. Use only for debugging,
/// information, stats, status.
std::unordered_map<ClientID, Client::Ptr> all_clients() const;
std::optional<Client::Ptr> get_client(ClientID id, bmp::State min_state) const;
std::unordered_map<VehicleID, Vehicle::Ptr> get_vehicles_owned_by(ClientID id);
std::optional<Vehicle::Ptr> get_vehicle(VehicleID id);
/// Builds the SessionSetup.PlayersInfo json which contains all player info and all vehicles.
nlohmann::json build_players_info();
size_t authenticated_client_count() const;
size_t clients_in_state_count(bmp::State state) const;
size_t guest_count() const;
size_t vehicle_count() const;
/// Creates a Playing state packet from uncompressed data.
bmp::Packet make_playing_packet(bmp::Purpose purpose, ClientID from_id, VehicleID veh_id, const std::vector<uint8_t>& data);
/// Sends a <System> or <Server> chat message to all or only one client(s).
void send_system_chat_message(const std::string& msg, ClientID to = 0xffffffff);
/// To be called by accept() async handler once an accept() is completed.
void accept();
/// Gets the async i/o context of the network - can be used to "schedule" tasks on it.
boost::asio::thread_pool& context() {
return m_threadpool;
}
private:
void handle_packet(ClientID id, const bmp::Packet& packet);
/// Reads a packet from the given UDP socket, returning the client's endpoint as an out-argument.
bmp::Packet udp_read(ip::udp::endpoint& out_ep);
/// Sends a packet to the specified UDP endpoint via the UDP socket.
void udp_write(bmp::Packet& packet, const ip::udp::endpoint& ep);
void udp_read_main();
void tcp_listen_main();
/// Handles all packets which are allowed during the Identification state.
void handle_identification(ClientID id, const bmp::Packet& packet, std::shared_ptr<Client>& client);
/// Handles all packets which are allowed during the Authentication state.
void handle_authentication(ClientID id, const bmp::Packet& packet, std::shared_ptr<Client>& client);
/// Handles all packets which are allowed during the ModDownload state.
void handle_mod_download(ClientID id, const bmp::Packet& packet, std::shared_ptr<Client>& client);
/// Handles all packets which are allowed during the SessionSetup state.
void handle_session_setup(ClientID id, const bmp::Packet& packet, std::shared_ptr<Client>& client);
/// Handles all packets which are allowed during the Playing state.
void handle_playing(ClientID id, const bmp::Packet& packet, std::shared_ptr<Client>& client);
/// On failure, throws an exception with the error for the client.
static void authenticate_user(const std::string& public_key, std::shared_ptr<Client>& client);
/// Called by accept() once completed (completion handler).
void handle_accept(const boost::system::error_code& ec);
Sync<std::unordered_map<ClientID, Client::Ptr>> m_clients {};
Sync<std::unordered_map<VehicleID, Vehicle::Ptr>> m_vehicles {};
Sync<std::unordered_map<uint64_t, ClientID>> m_client_magics {};
Sync<std::unordered_map<ip::udp::endpoint, ClientID>> m_udp_endpoints {};
ClientID new_client_id();
VehicleID new_vehicle_id();
thread_pool m_threadpool { std::thread::hardware_concurrency() };
Sync<bool> m_shutdown { false };
ip::udp::socket m_udp_socket { m_threadpool };
ip::tcp::socket m_tcp_listener { m_threadpool };
ip::tcp::acceptor m_tcp_acceptor { m_threadpool };
/// This socket gets accepted into, and is then moved.
ip::tcp::socket m_temp_socket { m_threadpool };
boost::scoped_thread<> m_tcp_listen_thread;
boost::scoped_thread<> m_udp_read_thread;
};
-68
View File
@@ -1,68 +0,0 @@
#pragma once
#include "Error.h"
#include "Value.h"
#include <filesystem>
#include <future>
#include <utility>
#include <variant>
/// The Plugin class is an abstract interface for any plugin.
///
/// A plugin must itself figure out how to deal with events, load itself,
/// and must react quickly and appropriately to any incoming events or function calls.
/// A plugin may *not* ever block a calling thread unless explicitly marked with
/// "this may block the caller" or similar.
class Plugin {
public:
/// Self-managing pointer type of this plugin.
using Pointer = std::shared_ptr<Plugin>;
/// Allocates a Plugin of the specific derived plugin type.
template <typename T, typename... Args>
static Pointer make_pointer(Args&&... args) {
return std::shared_ptr<Plugin>(new T(std::forward<Args>(args)...));
}
/// Default constructor to enable derived classes to default-construct.
Plugin() = default;
/// Plugin is not copyable.
Plugin(const Plugin&) = delete;
/// Plugin is not copyable.
Plugin& operator=(const Plugin&) = delete;
/// Plugin is movable.
Plugin(Plugin&&) = default;
/// Plugin is movable.
Plugin& operator=(Plugin&&) = default;
/// Default destructor but virtual, to make the compiler happy.
virtual ~Plugin() = default;
/// Called when the plugin should initialize its state.
/// This may block the caller.
virtual Error initialize() = 0;
/// Called when the plugin should tear down and clean up its state.
/// This may block the caller.
virtual Error cleanup() = 0;
/// Called when the plugin should be reloaded. Usually it's a good idea
/// to notify the plugin's code, call cleanup(), etc. internally.
virtual Error reload() = 0;
/// Returns the name of the plugin.
virtual std::string name() const = 0;
/// Returns the path to the plugin - this can either be the folder in which
/// the plugin's files live, or the plugin itself if it's a single file.
/// The exact format of what this returns (directory/file) is implementation defined.
virtual std::filesystem::path path() const = 0;
/// Instructs the plugin to handle the given event, with the given arguments.
/// Returns a future with a result if this event will be handled by the plugin, otherwise must return
/// std::nullopt.
virtual std::shared_future<std::vector<Value>> handle_event(const std::string& event_name, const std::shared_ptr<Value>& args) = 0;
/// Returns how much memory this state thinks it uses.
///
/// This value is difficult to calculate for some use-cases, but a rough ballpark
/// should be returned regardless.
virtual size_t memory_usage() const = 0;
};
-73
View File
@@ -1,73 +0,0 @@
#pragma once
#include "HashMap.h"
#include "Plugin.h"
/// The PluginManager class manages all plugins, specifically their lifetime,
/// events and memory.
class PluginManager {
public:
/// Iterates through all plugins, ask them for their usage, take the sum.
size_t memory_usage() const {
size_t total = 0;
auto plugins = m_plugins.synchronize();
for (const auto& [name, plugin] : *plugins) {
(void)name; // name ignored
total += plugin->memory_usage();
}
return total;
}
/// Triggers (sends) the event to all plugins and gathers the results as futures.
///
/// PLEASE be aware that some plugins' handlers may take a while to handle the event,
/// so try not to wait on these futures without a timeout.
///
/// This function should not block.
std::vector<std::shared_future<std::vector<Value>>> trigger_event(const std::string& event_name, const std::shared_ptr<Value>& args) {
// results will go in here
std::vector<std::shared_future<std::vector<Value>>> results;
// synchronize practically grabs a lock to the mutex, this is (as the name suggests)
// a synchronization point. technically, it could dead-lock if something that is called
// in this locked context tries to lock the m_plugins mutex.
// Plugin::handle_event should NEVER do much else than dispatch the event to the
// plugin's main thread, so this really cannot happen.
// that said, if you end up here with gdb, make sure it doesn't ;)
auto plugins = m_plugins.synchronize();
// allocate as many as we could possibly have, to avoid realloc()'s
results.reserve(plugins->size());
for (const auto& [name, plugin] : *plugins) {
(void)name; // ignore the name
// propagates the event to the plugin, this returns a future
// we assume that at this point no plugin-specific code has executed
auto result = plugin->handle_event(event_name, args);
// if the plugin had no handler, this result has no value, and we can ignore it
results.push_back(std::move(result));
}
return results;
}
/// Adds the plugin, calls Plugin::initialize(), and so on
[[nodiscard]] Error add_plugin(Plugin::Pointer plugin) {
auto plugins = m_plugins.synchronize();
if (plugins->contains(plugin->name())) {
return Error("Plugin with the name '{}' already exists, refusing to replace it.", plugin->name());
} else {
auto [iter, b] = plugins->insert({ plugin->name(), std::move(plugin) });
(void)b; // ignore b
auto err = iter->second->initialize();
if (err) {
return err;
}
return {};
}
}
HashMap<std::string, Plugin::Pointer> get_plugins() {
return *m_plugins;
}
private:
/// All plugins as pointers to allow inheritance.
SynchronizedHashMap<std::string, Plugin::Pointer> m_plugins;
};
+75
View File
@@ -0,0 +1,75 @@
#pragma once
#include <boost/thread/synchronized_value.hpp>
#include <chrono>
#include <cstddef>
#include <limits>
#include <unordered_map>
#undef max
#undef min
namespace prof {
using Duration = std::chrono::duration<double, std::milli>;
using TimePoint = std::chrono::high_resolution_clock::time_point;
/// Returns the current time.
TimePoint now();
/// Returns a sub-millisecond resolution duration between start and end.
Duration duration(const TimePoint& start, const TimePoint& end);
struct Stats {
double mean;
double stdev;
double min;
double max;
size_t n;
};
/// Calculates and stores the moving average over K samples of execution time data
/// for some single unit of code. Threadsafe.
struct UnitExecutionTime {
UnitExecutionTime();
/// Adds a sample to the collection, overriding the oldest sample if needed.
void add_sample(const Duration& dur);
/// Calculates the mean duration over the `measurement_count()` measurements,
/// as well as the standard deviation.
Stats stats() const;
/// Returns the number of elements the moving average is calculated over.
size_t measurement_count() const;
private:
mutable std::mutex m_mtx {};
size_t m_total_calls {};
double m_sum {};
// sum of measurements squared (for running stdev)
double m_measurement_sqr_sum {};
double m_min { std::numeric_limits<double>::max() };
double m_max { std::numeric_limits<double>::min() };
};
/// Holds profiles for multiple units by name. Threadsafe.
struct UnitProfileCollection {
/// Adds a sample to the collection, overriding the oldest sample if needed.
void add_sample(const std::string& unit, const Duration& duration);
/// Calculates the mean duration over the `measurement_count()` measurements,
/// as well as the standard deviation.
Stats stats(const std::string& unit);
/// Returns the number of elements the moving average is calculated over.
size_t measurement_count(const std::string& unit);
/// Returns the stats for all stored units.
std::unordered_map<std::string, Stats> all_stats();
private:
boost::synchronized_value<std::unordered_map<std::string, UnitExecutionTime>> m_map;
};
}
+1 -1
View File
@@ -23,8 +23,8 @@
* and write locks and read locks are mutually exclusive.
*/
#include <shared_mutex>
#include <mutex>
#include <shared_mutex>
// Use ReadLock(m) and WriteLock(m) to lock it.
using RWMutex = std::shared_mutex;
+144
View File
@@ -0,0 +1,144 @@
// BeamMP, the BeamNG.drive multiplayer mod.
// Copyright (C) 2024 BeamMP Ltd., BeamMP team and contributors.
//
// BeamMP Ltd. can be contacted by electronic mail via contact@beammp.com.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published
// by the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
#pragma once
#include "Sync.h"
#include <concepts>
#include <cstdint>
#include <doctest/doctest.h>
#include <fmt/core.h>
#include <fmt/format.h>
#include <stdexcept>
#include <string>
#include <unordered_map>
#include <variant>
struct ComposedKey {
std::string Category;
std::string Key;
bool operator==(const ComposedKey& rhs) const {
return (this->Category == rhs.Category && this->Key == rhs.Key);
}
};
template <>
struct fmt::formatter<ComposedKey> : formatter<std::string> {
auto format(ComposedKey key, format_context& ctx) const;
};
inline auto fmt::formatter<ComposedKey>::format(ComposedKey key, fmt::format_context& ctx) const {
std::string key_metadata = fmt::format("{}::{}", key.Category, key.Key);
return formatter<std::string>::format(key_metadata, ctx);
}
namespace std {
template <>
class hash<ComposedKey> {
public:
std::uint64_t operator()(const ComposedKey& key) const {
std::hash<std::string> hash_fn;
return hash_fn(key.Category + key.Key);
}
};
}
struct Settings {
using SettingsTypeVariant = std::variant<std::string, bool, int>;
Settings();
enum Key {
// Keys that correspond to the keys set in TOML
// Keys have their TOML section name as prefix
// [Misc]
Misc_ImScaredOfUpdates,
Misc_UpdateReminderTime,
// [General]
General_Description,
General_Tags,
General_MaxPlayers,
General_Name,
General_Map,
General_AuthKey,
General_Private,
General_IP,
General_Port,
General_MaxCars,
General_LogChat,
General_ResourceFolder,
General_Debug,
General_AllowGuests,
General_InformationPacket,
};
Sync<std::unordered_map<Key, SettingsTypeVariant>> SettingsMap;
enum SettingsAccessMask {
READ_ONLY, // Value can be read from console
READ_WRITE, // Value can be read and written to from console
NO_ACCESS // Value is inaccessible from console (no read OR write)
};
using SettingsAccessControl = std::pair<
Key, // The Key's corresponding enum encoding
SettingsAccessMask // Console read/write permissions
>;
Sync<std::unordered_map<ComposedKey, SettingsAccessControl>> InputAccessMapping;
std::string getAsString(Key key);
int getAsInt(Key key);
bool getAsBool(Key key);
SettingsTypeVariant get(Key key);
void set(Key key, const std::string& value);
template <typename Integer, std::enable_if_t<std::is_same_v<Integer, int>, bool> = true>
void set(Key key, Integer value) {
auto map = SettingsMap.synchronize();
if (!map->contains(key)) {
throw std::logic_error { "Undefined setting key accessed in Settings::set(int)" };
}
if (!std::holds_alternative<int>(map->at(key))) {
throw std::logic_error { fmt::format("Wrong value type in Settings::set(int): index {}", map->at(key).index()) };
}
map->at(key) = value;
}
template <typename Boolean, std::enable_if_t<std::is_same_v<bool, Boolean>, bool> = true>
void set(Key key, Boolean value) {
auto map = SettingsMap.synchronize();
if (!map->contains(key)) {
throw std::logic_error { "Undefined setting key accessed in Settings::set(bool)" };
}
if (!std::holds_alternative<bool>(map->at(key))) {
throw std::logic_error { fmt::format("Wrong value type in Settings::set(bool): index {}", map->at(key).index()) };
}
map->at(key) = value;
}
const std::unordered_map<ComposedKey, SettingsAccessControl> getAccessControlMap() const;
SettingsAccessControl getConsoleInputAccessMapping(const ComposedKey& keyName);
void setConsoleInputAccessMapping(const ComposedKey& keyName, const std::string& value);
void setConsoleInputAccessMapping(const ComposedKey& keyName, int value);
void setConsoleInputAccessMapping(const ComposedKey& keyName, bool value);
};
+1 -2
View File
@@ -5,6 +5,5 @@
/// This header provides convenience aliases for synchronization primitives.
template<typename T>
template <typename T>
using Sync = boost::synchronized_value<T, std::recursive_mutex>;
+4 -4
View File
@@ -19,6 +19,7 @@
#pragma once
#include "Common.h"
#include "Settings.h"
#include <atomic>
#include <filesystem>
@@ -35,18 +36,17 @@ public:
[[nodiscard]] bool Failed() const { return mFailed; }
void FlushToFile();
void PrintDebug();
private:
void CreateConfigFile();
void ParseFromFile(std::string_view name);
void PrintDebug();
void TryReadValue(toml::value& Table, const std::string& Category, const std::string_view& Key, const std::string_view& Env, std::string& OutValue);
void TryReadValue(toml::value& Table, const std::string& Category, const std::string_view& Key, const std::string_view& Env, bool& OutValue);
void TryReadValue(toml::value& Table, const std::string& Category, const std::string_view& Key, const std::string_view& Env, int& OutValue);
void TryReadValue(toml::value& Table, const std::string& Category, const std::string_view& Key, const std::string_view& Env, Settings::Key key);
void ParseOldFormat();
std::string TagsAsPrettyArray() const;
bool IsDefault();
bool mFailed { false };
bool mDisableConfig { false };
std::string mConfigFileName;
};
+13 -5
View File
@@ -19,8 +19,6 @@
#pragma once
#include "Cryptography.h"
#include "LuaPlugin.h"
#include "TScopedTimer.h"
#include "commandline.h"
#include <atomic>
#include <fstream>
@@ -31,6 +29,8 @@
#include <unordered_map>
#include <vector>
class TLuaEngine;
class TConsole {
public:
TConsole();
@@ -40,7 +40,7 @@ public:
void Write(const std::string& str);
void WriteRaw(const std::string& str);
// void InitializeLuaConsole(TLuaEngine& Engine);
void InitializeLuaConsole(TLuaEngine& Engine);
void BackupOldLog();
void StartLoggingToFile();
Commandline& Internal() { return *mCommandline; }
@@ -58,6 +58,10 @@ private:
void Command_Status(const std::string& cmd, const std::vector<std::string>& args);
void Command_Settings(const std::string& cmd, const std::vector<std::string>& args);
void Command_Clear(const std::string&, const std::vector<std::string>& args);
void Command_Version(const std::string& cmd, const std::vector<std::string>& args);
void Command_ProtectMod(const std::string& cmd, const std::vector<std::string>& args);
void Command_ReloadMods(const std::string& cmd, const std::vector<std::string>& args);
void Command_NetTest(const std::string& cmd, const std::vector<std::string>& args);
void Command_Say(const std::string& FullCommand);
bool EnsureArgsCount(const std::vector<std::string>& args, size_t n);
@@ -75,16 +79,20 @@ private:
{ "settings", [this](const auto& a, const auto& b) { Command_Settings(a, b); } },
{ "clear", [this](const auto& a, const auto& b) { Command_Clear(a, b); } },
{ "say", [this](const auto&, const auto&) { Command_Say(""); } }, // shouldn't actually be called
{ "version", [this](const auto& a, const auto& b) { Command_Version(a, b); } },
{ "protectmod", [this](const auto& a, const auto& b) { Command_ProtectMod(a, b); } },
{ "reloadmods", [this](const auto& a, const auto& b) { Command_ReloadMods(a, b); } },
{ "nettest", [this](const auto& a, const auto& b) { Command_NetTest(a, b); } },
};
std::unique_ptr<Commandline> mCommandline { nullptr };
std::vector<std::string> mCachedLuaHistory;
std::vector<std::string> mCachedRegularHistory;
TLuaEngine* mLuaEngine { nullptr };
bool mIsLuaConsole { false };
bool mFirstTime { true };
std::string mStateId;
const std::string mDefaultStateId = BEAMMP_MEMORY_STATE;
const std::string mDefaultStateId = "BEAMMP_SERVER_CONSOLE";
std::ofstream mLogFileStream;
std::mutex mLogFileStreamMtx;
TScopedTimer mUptimeTimer{};
};
+6 -3
View File
@@ -20,17 +20,20 @@
#include "Common.h"
#include "IThreaded.h"
#include "Network.h"
#include "TResourceManager.h"
#include "TServer.h"
class THeartbeatThread : public IThreaded {
public:
THeartbeatThread(std::shared_ptr<Network> network);
THeartbeatThread(TResourceManager& ResourceManager, TServer& Server);
//~THeartbeatThread();
void operator()() override;
static inline std::string lastCall = "";
private:
std::string GenerateCall();
std::string GetPlayers();
std::shared_ptr<Network> m_network;
TResourceManager& mResourceManager;
TServer& mServer;
};
+321
View File
@@ -0,0 +1,321 @@
// BeamMP, the BeamNG.drive multiplayer mod.
// Copyright (C) 2024 BeamMP Ltd., BeamMP team and contributors.
//
// BeamMP Ltd. can be contacted by electronic mail via contact@beammp.com.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published
// by the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
#pragma once
#include "Profiling.h"
#include "TNetwork.h"
#include "TServer.h"
#include <any>
#include <chrono>
#include <condition_variable>
#include <filesystem>
#include <initializer_list>
#include <list>
#include <lua.hpp>
#include <memory>
#include <mutex>
#include <nlohmann/json.hpp>
#include <queue>
#include <random>
#include <set>
#include <toml.hpp>
#include <unordered_map>
#include <vector>
#define SOL_ALL_SAFETIES_ON 1
#define SOL_USER_C_ASSERT SOL_ON
#define SOL_C_ASSERT(...) \
beammp_lua_errorf("SOL2 assertion failure: Assertion `{}` failed in {}:{}. This *should* be a fatal error, but BeamMP Server overrides it to not be fatal. This may cause the Lua Engine to crash, or cause other issues.", #__VA_ARGS__, __FILE__, __LINE__)
#include <sol/sol.hpp>
struct JsonString {
std::string value;
};
// value used to keep nils in a table or array, across serialization boundaries like
// JsonEncode, so that the nil stays at the same index and isn't treated like a special
// value (e.g. one that can be ignored or discarded).
const inline std::string BEAMMP_INTERNAL_NIL = "BEAMMP_SERVER_INTERNAL_NIL_VALUE";
using TLuaStateId = std::string;
namespace fs = std::filesystem;
/**
* std::variant means, that TLuaArgTypes may be one of the Types listed as template args
*/
using TLuaValue = std::variant<std::string, int, JsonString, bool, std::unordered_map<std::string, std::string>, float>;
enum TLuaType {
String = 0,
Int = 1,
Json = 2,
Bool = 3,
StringStringMap = 4,
Float = 5,
};
class TLuaPlugin;
struct TLuaResult {
bool Ready;
bool Error;
std::string ErrorMessage;
sol::object Result { sol::lua_nil };
TLuaStateId StateId;
std::string Function;
std::shared_ptr<std::mutex> ReadyMutex {
std::make_shared<std::mutex>()
};
std::shared_ptr<std::condition_variable> ReadyCondition {
std::make_shared<std::condition_variable>()
};
void MarkAsReady();
void WaitUntilReady();
};
struct TLuaPluginConfig {
static inline const std::string FileName = "PluginConfig.toml";
TLuaStateId StateId;
// TODO: Add execute list
// TODO: Build a better toml serializer, or some way to do this in an easier way
};
struct TLuaChunk {
TLuaChunk(std::shared_ptr<std::string> Content,
std::string FileName,
std::string PluginPath);
std::shared_ptr<std::string> Content;
std::string FileName;
std::string PluginPath;
};
class TLuaEngine : public std::enable_shared_from_this<TLuaEngine>, IThreaded {
public:
enum CallStrategy : int {
BestEffort,
Precise,
};
struct QueuedFunction {
std::string FunctionName;
std::shared_ptr<TLuaResult> Result;
std::vector<TLuaValue> Args;
std::string EventName; // optional, may be empty
};
TLuaEngine();
virtual ~TLuaEngine() noexcept {
beammp_debug("Lua Engine terminated");
}
void operator()() override;
TNetwork& Network() { return *mNetwork; }
TServer& Server() { return *mServer; }
void SetNetwork(TNetwork* Network) { mNetwork = Network; }
void SetServer(TServer* Server) { mServer = Server; }
size_t GetResultsToCheckSize() {
std::unique_lock Lock(mResultsToCheckMutex);
return mResultsToCheck.size();
}
size_t GetLuaStateCount() {
std::unique_lock Lock(mLuaStatesMutex);
return mLuaStates.size();
}
std::vector<std::string> GetLuaStateNames() {
std::vector<std::string> names {};
for (auto const& [stateId, _] : mLuaStates) {
names.push_back(stateId);
}
return names;
}
size_t GetTimedEventsCount() {
std::unique_lock Lock(mTimedEventsMutex);
return mTimedEvents.size();
}
size_t GetRegisteredEventHandlerCount() {
std::unique_lock Lock(mLuaEventsMutex);
size_t LuaEventsCount = 0;
for (const auto& State : mLuaEvents) {
for (const auto& Events : State.second) {
LuaEventsCount += Events.second.size();
}
}
return LuaEventsCount - GetLuaStateCount();
}
static void WaitForAll(std::vector<std::shared_ptr<TLuaResult>>& Results,
const std::optional<std::chrono::high_resolution_clock::duration>& Max = std::nullopt);
void ReportErrors(const std::vector<std::shared_ptr<TLuaResult>>& Results);
bool HasState(TLuaStateId StateId);
[[nodiscard]] std::shared_ptr<TLuaResult> EnqueueScript(TLuaStateId StateID, const TLuaChunk& Script);
[[nodiscard]] std::shared_ptr<TLuaResult> EnqueueFunctionCall(TLuaStateId StateID, const std::string& FunctionName, const std::vector<TLuaValue>& Args, const std::string& EventName);
void EnsureStateExists(TLuaStateId StateId, const std::string& Name, bool DontCallOnInit = false);
void RegisterEvent(const std::string& EventName, TLuaStateId StateId, const std::string& FunctionName);
/**
*
* @tparam ArgsT Template Arguments for the event (Metadata) todo: figure out what this means
* @param EventName Name of the event
* @param IgnoreId
* @param Args
* @return
*/
template <typename... ArgsT>
[[nodiscard]] std::vector<std::shared_ptr<TLuaResult>> TriggerEvent(const std::string& EventName, TLuaStateId IgnoreId, ArgsT&&... Args) {
std::unique_lock Lock(mLuaEventsMutex);
beammp_event(EventName);
if (mLuaEvents.find(EventName) == mLuaEvents.end()) { // if no event handler is defined for 'EventName', return immediately
return {};
}
std::vector<std::shared_ptr<TLuaResult>> Results;
std::vector<TLuaValue> Arguments { TLuaValue { std::forward<ArgsT>(Args) }... };
for (const auto& Event : mLuaEvents.at(EventName)) {
for (const auto& Function : Event.second) {
if (Event.first != IgnoreId) {
Results.push_back(EnqueueFunctionCall(Event.first, Function, Arguments, EventName));
}
}
}
return Results; //
}
template <typename... ArgsT>
[[nodiscard]] std::vector<std::shared_ptr<TLuaResult>> TriggerLocalEvent(const TLuaStateId& StateId, const std::string& EventName, ArgsT&&... Args) {
std::unique_lock Lock(mLuaEventsMutex);
beammp_event(EventName + " in '" + StateId + "'");
if (mLuaEvents.find(EventName) == mLuaEvents.end()) { // if no event handler is defined for 'EventName', return immediately
return {};
}
std::vector<std::shared_ptr<TLuaResult>> Results;
std::vector<TLuaValue> Arguments { TLuaValue { std::forward<ArgsT>(Args) }... };
const auto Handlers = GetEventHandlersForState(EventName, StateId);
for (const auto& Handler : Handlers) {
Results.push_back(EnqueueFunctionCall(StateId, Handler, Arguments, EventName));
}
return Results;
}
std::set<std::string> GetEventHandlersForState(const std::string& EventName, TLuaStateId StateId);
void CreateEventTimer(const std::string& EventName, TLuaStateId StateId, size_t IntervalMS, CallStrategy Strategy);
void CancelEventTimers(const std::string& EventName, TLuaStateId StateId);
sol::state_view GetStateForPlugin(const fs::path& PluginPath);
TLuaStateId GetStateIDForPlugin(const fs::path& PluginPath);
void AddResultToCheck(const std::shared_ptr<TLuaResult>& Result);
static constexpr const char* BeamMPFnNotFoundError = "BEAMMP_FN_NOT_FOUND";
std::vector<std::string> GetStateGlobalKeysForState(TLuaStateId StateId);
std::vector<std::string> GetStateTableKeysForState(TLuaStateId StateId, std::vector<std::string> keys);
// Debugging functions (slow)
std::unordered_map<std::string /*event name */, std::vector<std::string> /* handlers */> Debug_GetEventsForState(TLuaStateId StateId);
std::queue<std::pair<TLuaChunk, std::shared_ptr<TLuaResult>>> Debug_GetStateExecuteQueueForState(TLuaStateId StateId);
std::vector<QueuedFunction> Debug_GetStateFunctionQueueForState(TLuaStateId StateId);
std::vector<TLuaResult> Debug_GetResultsToCheckForState(TLuaStateId StateId);
private:
void CollectAndInitPlugins();
void InitializePlugin(const fs::path& Folder, const TLuaPluginConfig& Config);
void FindAndParseConfig(const fs::path& Folder, TLuaPluginConfig& Config);
size_t CalculateMemoryUsage();
class StateThreadData : IThreaded {
public:
StateThreadData(const std::string& Name, TLuaStateId StateId, TLuaEngine& Engine);
StateThreadData(const StateThreadData&) = delete;
virtual ~StateThreadData() noexcept { beammp_debug("\"" + mStateId + "\" destroyed"); }
[[nodiscard]] std::shared_ptr<TLuaResult> EnqueueScript(const TLuaChunk& Script);
[[nodiscard]] std::shared_ptr<TLuaResult> EnqueueFunctionCall(const std::string& FunctionName, const std::vector<TLuaValue>& Args, const std::string& EventName);
[[nodiscard]] std::shared_ptr<TLuaResult> EnqueueFunctionCallFromCustomEvent(const std::string& FunctionName, const std::vector<TLuaValue>& Args, const std::string& EventName, CallStrategy Strategy);
void RegisterEvent(const std::string& EventName, const std::string& FunctionName);
void AddPath(const fs::path& Path); // to be added to path and cpath
void operator()() override;
sol::state_view State() { return sol::state_view(mState); }
std::vector<std::string> GetStateGlobalKeys();
std::vector<std::string> GetStateTableKeys(const std::vector<std::string>& keys);
// Debug functions, slow
std::queue<std::pair<TLuaChunk, std::shared_ptr<TLuaResult>>> Debug_GetStateExecuteQueue();
std::vector<TLuaEngine::QueuedFunction> Debug_GetStateFunctionQueue();
private:
sol::table Lua_TriggerGlobalEvent(const std::string& EventName, sol::variadic_args EventArgs);
sol::table Lua_TriggerLocalEvent(const std::string& EventName, sol::variadic_args EventArgs);
sol::table Lua_GetPlayerIdentifiers(int ID);
std::variant<std::string, sol::nil_t> Lua_GetPlayerRole(int ID);
sol::table Lua_GetPlayers();
std::string Lua_GetPlayerName(int ID);
sol::table Lua_GetPlayerVehicles(int ID);
std::pair<sol::table, std::string> Lua_GetPositionRaw(int PID, int VID);
sol::table Lua_HttpCreateConnection(const std::string& host, uint16_t port);
sol::table Lua_JsonDecode(const std::string& str);
int Lua_GetPlayerIDByName(const std::string& Name);
sol::table Lua_FS_ListFiles(const std::string& Path);
sol::table Lua_FS_ListDirectories(const std::string& Path);
prof::UnitProfileCollection mProfile {};
std::unordered_map<std::string, prof::TimePoint> mProfileStarts;
std::string mName;
TLuaStateId mStateId;
lua_State* mState;
std::thread mThread;
std::queue<std::pair<TLuaChunk, std::shared_ptr<TLuaResult>>> mStateExecuteQueue;
std::recursive_mutex mStateExecuteQueueMutex;
std::vector<QueuedFunction> mStateFunctionQueue;
std::mutex mStateFunctionQueueMutex;
std::condition_variable mStateFunctionQueueCond;
TLuaEngine* mEngine;
sol::state_view mStateView { mState };
std::queue<fs::path> mPaths;
std::recursive_mutex mPathsMutex;
std::mt19937 mMersenneTwister;
std::uniform_real_distribution<double> mUniformRealDistribution01;
std::vector<sol::object> JsonStringToArray(JsonString Str);
};
struct TimedEvent {
std::chrono::high_resolution_clock::duration Duration {};
std::chrono::high_resolution_clock::time_point LastCompletion {};
std::string EventName;
TLuaStateId StateId;
CallStrategy Strategy;
bool Expired();
void Reset();
};
TNetwork* mNetwork;
TServer* mServer;
const fs::path mResourceServerPath;
std::vector<std::shared_ptr<TLuaPlugin>> mLuaPlugins;
std::unordered_map<TLuaStateId, std::unique_ptr<StateThreadData>> mLuaStates;
std::recursive_mutex mLuaStatesMutex;
std::unordered_map<std::string /* event name */, std::unordered_map<TLuaStateId, std::set<std::string>>> mLuaEvents;
std::recursive_mutex mLuaEventsMutex;
std::vector<TimedEvent> mTimedEvents;
std::recursive_mutex mTimedEventsMutex;
std::list<std::shared_ptr<TLuaResult>> mResultsToCheck;
std::mutex mResultsToCheckMutex;
std::condition_variable mResultsToCheckCond;
};
// std::any TriggerLuaEvent(const std::string& Event, bool local, TLuaPlugin* Caller, std::shared_ptr<TLuaArg> arg, bool Wait);
+37
View File
@@ -0,0 +1,37 @@
// BeamMP, the BeamNG.drive multiplayer mod.
// Copyright (C) 2024 BeamMP Ltd., BeamMP team and contributors.
//
// BeamMP Ltd. can be contacted by electronic mail via contact@beammp.com.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published
// by the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
#include "TLuaEngine.h"
class TLuaPlugin {
public:
TLuaPlugin(TLuaEngine& Engine, const TLuaPluginConfig& Config, const fs::path& MainFolder);
TLuaPlugin(const TLuaPlugin&) = delete;
TLuaPlugin& operator=(const TLuaPlugin&) = delete;
~TLuaPlugin() noexcept = default;
const TLuaPluginConfig& GetConfig() const { return mConfig; }
fs::path GetFolder() const { return mFolder; }
private:
TLuaPluginConfig mConfig;
TLuaEngine& mEngine;
fs::path mFolder;
std::string mPluginName;
std::unordered_map<std::string, std::shared_ptr<std::string>> mFileContents;
};
+76
View File
@@ -0,0 +1,76 @@
// BeamMP, the BeamNG.drive multiplayer mod.
// Copyright (C) 2024 BeamMP Ltd., BeamMP team and contributors.
//
// BeamMP Ltd. can be contacted by electronic mail via contact@beammp.com.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published
// by the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
#pragma once
#include "BoostAliases.h"
#include "Compat.h"
#include "TResourceManager.h"
#include "TServer.h"
#include <boost/asio/io_context.hpp>
#include <boost/asio/ip/udp.hpp>
struct TConnection;
class TNetwork {
public:
TNetwork(TServer& Server, TPPSMonitor& PPSMonitor, TResourceManager& ResourceManager);
[[nodiscard]] bool TCPSend(TClient& c, const std::vector<uint8_t>& Data, bool IsSync = false);
[[nodiscard]] bool SendLarge(TClient& c, std::vector<uint8_t> Data, bool isSync = false);
[[nodiscard]] bool Respond(TClient& c, const std::vector<uint8_t>& MSG, bool Rel, bool isSync = false);
std::shared_ptr<TClient> CreateClient(ip::tcp::socket&& TCPSock);
std::vector<uint8_t> TCPRcv(TClient& c);
void ClientKick(TClient& c, const std::string& R);
[[nodiscard]] bool SyncClient(const std::weak_ptr<TClient>& c);
void Identify(TConnection&& client);
std::shared_ptr<TClient> Authentication(TConnection&& ClientConnection);
void SyncResources(TClient& c);
[[nodiscard]] bool UDPSend(TClient& Client, std::vector<uint8_t> Data);
void SendToAll(TClient* c, const std::vector<uint8_t>& Data, bool Self, bool Rel);
void UpdatePlayer(TClient& Client);
TResourceManager& ResourceManager() const { return mResourceManager; }
private:
void UDPServerMain();
void TCPServerMain();
TServer& mServer;
TPPSMonitor& mPPSMonitor;
ip::udp::socket mUDPSock;
TResourceManager& mResourceManager;
std::thread mUDPThread;
std::thread mTCPThread;
std::mutex mOpenIDMutex;
std::vector<uint8_t> UDPRcvFromClient(ip::udp::endpoint& ClientEndpoint);
void OnConnect(const std::weak_ptr<TClient>& c);
void TCPClient(const std::weak_ptr<TClient>& c);
void Looper(const std::weak_ptr<TClient>& c);
int OpenID();
void OnDisconnect(const std::weak_ptr<TClient>& ClientPtr);
void Parse(TClient& c, const std::vector<uint8_t>& Packet);
void SendFile(TClient& c, const std::string& Name);
static bool TCPSendRaw(TClient& C, ip::tcp::socket& socket, const uint8_t* Data, size_t Size);
static void SendFileToClient(TClient& c, size_t Size, const std::string& Name);
static const uint8_t* SendSplit(TClient& c, ip::tcp::socket& Socket, const uint8_t* DataPtr, size_t Size);
};
std::string HashPassword(const std::string& str);
std::vector<uint8_t> StringToVector(const std::string& Str);
+45
View File
@@ -0,0 +1,45 @@
// BeamMP, the BeamNG.drive multiplayer mod.
// Copyright (C) 2024 BeamMP Ltd., BeamMP team and contributors.
//
// BeamMP Ltd. can be contacted by electronic mail via contact@beammp.com.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published
// by the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
#pragma once
#include "Common.h"
#include "TServer.h"
#include <optional>
class TNetwork;
class TPPSMonitor : public IThreaded {
public:
explicit TPPSMonitor(TServer& Server);
virtual ~TPPSMonitor() { }
void operator()() override;
void SetInternalPPS(int NewPPS) { mInternalPPS = NewPPS; }
void IncrementInternalPPS() { ++mInternalPPS; }
[[nodiscard]] int InternalPPS() const { return mInternalPPS; }
void SetNetwork(TNetwork& Server) { mNetwork = std::ref(Server); }
private:
TNetwork& Network() { return mNetwork->get(); }
TServer& mServer;
std::optional<std::reference_wrapper<TNetwork>> mNetwork { std::nullopt };
int mInternalPPS { 0 };
};
+40
View File
@@ -0,0 +1,40 @@
// BeamMP, the BeamNG.drive multiplayer mod.
// Copyright (C) 2024 BeamMP Ltd., BeamMP team and contributors.
//
// BeamMP Ltd. can be contacted by electronic mail via contact@beammp.com.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published
// by the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
#pragma once
#include "Common.h"
#include "IThreaded.h"
#include <atomic>
#include <memory>
#include <unordered_map>
class TLuaEngine;
class TPluginMonitor : IThreaded, public std::enable_shared_from_this<TPluginMonitor> {
public:
TPluginMonitor(const fs::path& Path, std::shared_ptr<TLuaEngine> Engine);
void operator()();
private:
std::shared_ptr<TLuaEngine> mEngine;
fs::path mPath;
std::unordered_map<std::string, fs::file_time_type> mFileTimes;
};
+47
View File
@@ -0,0 +1,47 @@
// BeamMP, the BeamNG.drive multiplayer mod.
// Copyright (C) 2024 BeamMP Ltd., BeamMP team and contributors.
//
// BeamMP Ltd. can be contacted by electronic mail via contact@beammp.com.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published
// by the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
#pragma once
#include "Common.h"
#include <nlohmann/json.hpp>
class TResourceManager {
public:
TResourceManager();
[[nodiscard]] size_t MaxModSize() const { return mMaxModSize; }
[[nodiscard]] std::string FileList() const { return mFileList; }
[[nodiscard]] std::string TrimmedList() const { return mTrimmedList; }
[[nodiscard]] std::string FileSizes() const { return mFileSizes; }
[[nodiscard]] int ModsLoaded() const { return mModsLoaded; }
[[nodiscard]] nlohmann::json GetMods() const { return mMods; }
void RefreshFiles();
void SetProtected(const std::string& ModName, bool Protected);
private:
size_t mMaxModSize = 0;
std::string mFileSizes;
std::string mFileList;
std::string mTrimmedList;
int mModsLoaded = 0;
std::mutex mModsMutex;
nlohmann::json mMods = nlohmann::json::array();
};
+73
View File
@@ -0,0 +1,73 @@
// BeamMP, the BeamNG.drive multiplayer mod.
// Copyright (C) 2024 BeamMP Ltd., BeamMP team and contributors.
//
// BeamMP Ltd. can be contacted by electronic mail via contact@beammp.com.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published
// by the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
#pragma once
#include "IThreaded.h"
#include "RWMutex.h"
#include "TScopedTimer.h"
#include <functional>
#include <memory>
#include <mutex>
#include <unordered_set>
#include "BoostAliases.h"
class TClient;
class TNetwork;
class TPPSMonitor;
class TServer final {
public:
using TClientSet = std::unordered_set<std::shared_ptr<TClient>>;
TServer(const std::vector<std::string_view>& Arguments);
void InsertClient(const std::shared_ptr<TClient>& Ptr);
void RemoveClient(const std::weak_ptr<TClient>&);
// in Fn, return true to continue, return false to break
void ForEachClient(const std::function<bool(std::weak_ptr<TClient>)>& Fn);
size_t ClientCount() const;
void GlobalParser(const std::weak_ptr<TClient>& Client, std::vector<uint8_t>&& Packet, TPPSMonitor& PPSMonitor, TNetwork& Network, bool udp);
static void HandleEvent(TClient& c, const std::string& Data);
RWMutex& GetClientMutex() const { return mClientsMutex; }
const TScopedTimer UptimeTimer;
// asio io context
io_context& IoCtx() { return mIoCtx; }
private:
io_context mIoCtx {};
TClientSet mClients;
mutable RWMutex mClientsMutex;
static void ParseVehicle(TClient& c, const std::string& Pckt, TNetwork& Network);
static bool ShouldSpawn(TClient& c, const std::string& CarJson, int ID);
static bool IsUnicycle(TClient& c, const std::string& CarJson);
static void Apply(TClient& c, int VID, const std::string& pckt);
void HandlePosition(TClient& c, const std::string& Packet);
};
struct BufferView {
uint8_t* Data { nullptr };
size_t Size { 0 };
const uint8_t* data() const { return Data; }
uint8_t* data() { return Data; }
size_t size() const { return Size; }
};
-269
View File
@@ -1,269 +0,0 @@
#pragma once
/// @file
/// The Value.h file describes a collection of wrapper types for use in
/// cross-plugin communication and similar. These wrapper types are
/// typically not zero-cost, so be careful and use these sparigly.
///
/// Base visitors, such as ValueToStringVisitor, should be declared
/// here also.
#include "Error.h"
#include "HashMap.h"
#include "boost/variant/variant_fwd.hpp"
#include <boost/json.hpp>
#include <boost/variant.hpp>
#include <boost/variant/variant.hpp>
#include <ostream>
#include <sol/forward.hpp>
#include <string>
#include <vector>
/// Dynamic array, can resize.
template<typename T>
using Array = std::vector<T>;
/// Null value, for use in Value.
struct Null {
/// Makes a null value. It's an identity value,
/// so its existance is the value.
explicit Null() { }
};
/// Formats "null".
std::ostream& operator<<(std::ostream& os, const Null&);
/// Contains a boolean value, for use in Value,
/// as booleans will be implicitly converted to int.
struct Bool {
/// Construct a bool from a boolean.
explicit Bool(bool b_)
: b(b_) { }
/// Contained value.
bool b;
/// Implicit conversion to bool, because it's expected to work this way.
operator bool() const { return b; }
};
template<typename T>
struct Tuple final : public Array<T> {
using Array<T>::Array;
};
/// Formats to "true" or "false".
std::ostream& operator<<(std::ostream& os, const Bool&);
/// The Value type is a recursively defined variant, which allows
/// passing a single value with any of a selection of types, including
/// the possibility to pass hashmaps of hashmaps of hashmaps of types (and so on).
///
/// In common pseudo-C++, this would be written as:
///
/// \code{.cpp}
/// using Value = variant<string, int, double, HashMap<string, Value>;
/// // ^^^^^
/// \endcode
/// Note the `^^^` annotated recursion. This isn't directly possible in C++,
/// so we use boost's recursive variants for this. Documentation is here
/// https://www.boost.org/doc/libs/1_82_0/doc/html/variant/tutorial.html#variant.tutorial.recursive
///
/// The use-case of a Value is to represent almost any primitive-ish type we may get from, or
/// may want to pass to, a Plugin.
///
/// For example, a table of key-value pairs, or a table of tables, or just a string, or a float, could all
/// be represented by this.
///
/// See the abstract template class ValueVisitor for how to access this with the
/// visitor pattern.
using Value = boost::make_recursive_variant<
std::string,
int64_t,
double,
Null,
Array<boost::recursive_variant_>,
HashMap<std::string, boost::recursive_variant_>,
Bool,
Tuple<boost::recursive_variant_>>::type;
// the following VALUE_TYPE_* variables are used mostly for
// unit-tests and code that can't use visitors.
/// Index of string in Value
[[maybe_unused]] constexpr int VALUE_TYPE_IDX_STRING = 0;
/// Index of int in Value
[[maybe_unused]] constexpr int VALUE_TYPE_IDX_INT = 1;
/// Index of double in Value
[[maybe_unused]] constexpr int VALUE_TYPE_IDX_DOUBLE = 2;
/// Index of null in Value
[[maybe_unused]] constexpr int VALUE_TYPE_IDX_NULL = 3;
/// Index of array in Value
[[maybe_unused]] constexpr int VALUE_TYPE_IDX_ARRAY = 4;
/// Index of hashmap in Value
[[maybe_unused]] constexpr int VALUE_TYPE_IDX_HASHMAP = 5;
/// Index of bool in Value
[[maybe_unused]] constexpr int VALUE_TYPE_IDX_BOOL = 6;
/// Index of tuple in Value
[[maybe_unused]] constexpr int VALUE_TYPE_IDX_TUPLE = 7;
/// A handy typedef for the recursive HashMap type inside a Value.
/// You may have to use this in order to make the compiler understand
/// what kind of value (a hash map) you are constructing.
using ValueHashMap = HashMap<std::string, Value>;
/// A handy typedef for the recursive Array type inside a Value.
/// You may have to use this in order to make the compiler understand
/// what kind of value (an array) you are constructing.
using ValueArray = Array<Value>;
/// A handy dandy typedef for using a tuple of values.
using ValueTuple = Tuple<Value>;
/// The ValueVisitor class is an abstract interface which allows the implementation
/// to easily construct a visitor for a Value object.
///
/// A Value object is a recursive variant class, and as such it's not simple to access
/// (no variants are really trivial to access). The visitor pattern gives us a type-safe
/// way to access such a variant, and the boost::static_visitor pattern does so in a
/// pretty concise way.
///
/// An example use is the ValueToStringVisitor.
template<typename ResultT>
class ValueVisitor : public boost::static_visitor<ResultT> {
public:
/// Needs to be default-constructible for the standard use case (see example above).
ValueVisitor() = default;
/// Cannot be copied.
ValueVisitor(const ValueVisitor&) = delete;
/// Cannot be copied.
ValueVisitor& operator=(const ValueVisitor&) = delete;
/// Virtual destructor is needed for virtual classes.
virtual ~ValueVisitor() = default;
/// ResultT from string.
virtual ResultT operator()(const std::string& str) const = 0;
/// ResultT from integer.
virtual ResultT operator()(int64_t i) const = 0;
/// ResultT from float.
virtual ResultT operator()(double d) const = 0;
/// ResultT from null.
virtual ResultT operator()(Null null) const = 0;
/// ResultT from boolean.
virtual ResultT operator()(Bool b) const = 0;
/// ResultT from array of values (must recursively visit).
virtual ResultT operator()(const ValueArray& array) const = 0;
/// ResultT from tuple of values (must recursively visit).
virtual ResultT operator()(const ValueTuple& array) const = 0;
/// ResultT from hashmap of values (must recursively visit).
virtual ResultT operator()(const HashMap<std::string, Value>& map) const = 0;
};
/// The ValueToStringVisitor class implements a visitor for a Value which
/// turns it into a human-readable string.
///
/// Example
/// \code{.cpp}
/// #include <boost/variant.hpp>
///
/// Value value = ...;
///
/// std::string str = boost::apply_visitor(ValueToStringVisitor(), value);
/// // ^--------------------^ ^---^
/// // default ctor |
/// // value to visit
/// \endcode
class ValueToStringVisitor : public ValueVisitor<std::string> {
public:
/// Flag used to specify behavior of ValueToStringVisitor.
enum Flag {
/// No options
NONE = 0,
/// Quote strings, `value` becomes `"value"`.
QUOTE_STRINGS = 0b1,
};
/// Constructs a ValueToStringVisitor with options.
/// With flags you can change, for example, whether strings should be quoted
/// when they standalone.
/// Depth is used by recursion, ignore it.
explicit ValueToStringVisitor(Flag flags = QUOTE_STRINGS, int depth = 1);
/// Returns the same string, possibly quoted (depends on flags).
std::string operator()(const std::string& str) const;
/// Uses fmt::format() to stringify the integer.
std::string operator()(int64_t i) const;
/// Uses fmt::format() to stringify the double.
std::string operator()(double d) const;
/// Returns "null".
std::string operator()(Null null) const;
/// Returns "true" or "false".
std::string operator()(Bool b) const;
/// Returns an object of format [ value, value, value ].
/// Recursively visits the elements of the array.
std::string operator()(const ValueArray& array) const;
/// Returns a tuple of format ( value, value, value ).
/// Recursively visits the elements of the array.
std::string operator()(const ValueTuple& array) const;
/// Returns an object of format { key: value, key: value }.
/// Recursively visits the elements of the map.
std::string operator()(const HashMap<std::string, Value>& map) const;
private:
/// Whether to quote strings before output.
bool m_quote_strings;
/// How many 2-space "tabs" to use - used by recursion.
int m_depth;
};
/// The ValueToJsonVisitor class is used to convert a Value into
/// a boost::json object.
class ValueToJsonVisitor : public ValueVisitor<boost::json::value> {
public:
/// Converts to json string.
boost::json::value operator()(const std::string& str) const;
/// Converts to json integer.
boost::json::value operator()(int64_t i) const;
/// Converts to json float.
boost::json::value operator()(double d) const;
/// Converts to empty json value.
boost::json::value operator()(Null null) const;
/// Converts to json boolean.
boost::json::value operator()(Bool b) const;
/// Converts to json array.
boost::json::value operator()(const ValueArray& array) const;
/// Converts to json array (because tuples don't exist).
boost::json::value operator()(const ValueTuple& array) const;
/// Converts to json object.
boost::json::value operator()(const HashMap<std::string, Value>& map) const;
};
/// The ValueToLuaVisitor class is used to convert a Value into a
/// sol object.
class ValueToLuaVisitor : public ValueVisitor<sol::object> {
public:
/// ValueToLuaVisitor needs a sol state in order to construct objects.
ValueToLuaVisitor(sol::state& state);
sol::object operator()(const std::string& str) const;
sol::object operator()(int64_t i) const;
sol::object operator()(double d) const;
sol::object operator()(Null null) const;
sol::object operator()(Bool b) const;
sol::object operator()(const ValueArray& array) const;
sol::object operator()(const ValueTuple& array) const;
sol::object operator()(const HashMap<std::string, Value>& map) const;
private:
sol::state& m_state;
};
/// This function converts from a lua (sol) wrapped value into a beammp value, for use in C++.
///
/// Value is a type which can be passed around between threads, and has no external dependencies.
/// Sol values are not like that, as they are references to stack indices in lua, and similar.
///
/// This function is also used to print values, by first converting them to a Value, then using a
/// ValueToStringVisitor.
///
/// The second argument is a provider for values which the function can't convert.
/// "invalid provider" means "provider of values for invalid sol values". If nullptr, then
/// any invalid value (such as a function) will be resolved to an error instead and the function will
/// fail.
Result<Value> sol_obj_to_value(const sol::object&, const std::function<Result<Value>(const sol::object&)>& invalid_provider = nullptr, size_t max_depth = 50);
+7 -4
View File
@@ -18,11 +18,12 @@
#pragma once
#include <nlohmann/json.hpp>
#include <string>
class TVehicleData final {
public:
TVehicleData(int ID, std::string Data);
TVehicleData(int ID, nlohmann::json Data);
~TVehicleData();
// We cannot delete this, since vector needs to be able to copy when it resizes.
// Deleting this causes some wacky template errors which are hard to decipher,
@@ -32,14 +33,16 @@ public:
[[nodiscard]] bool IsInvalid() const { return mID == -1; }
[[nodiscard]] int ID() const { return mID; }
[[nodiscard]] std::string Data() const { return mData; }
void SetData(const std::string& Data) { mData = Data; }
[[nodiscard]] nlohmann::json Data() const { return mData; }
[[nodiscard]] std::string DataAsPacket(const std::string& Role, const std::string& Name, int ID) const;
void SetData(const nlohmann::json& Data) { mData = Data; }
bool operator==(const TVehicleData& v) const { return mID == v.mID; }
private:
int mID { -1 };
std::string mData;
nlohmann::json mData;
};
// TODO: unused now, remove?
+1 -1
View File
@@ -4,4 +4,4 @@ set -ex
./vcpkg/bootstrap-vcpkg.sh
cmake . -B bin $1 -DCMAKE_BUILD_TYPE=Release -DCMAKE_CXX_FLAGS="-O3 -g -Wl,-z,norelro -Wl,--hash-style=gnu -Wl,-z,noseparate-code -ffunction-sections -fdata-sections -Wl,--gc-sections" -DBeamMP-Server_ENABLE_LTO=ON
cmake . -B bin $1 -DCMAKE_BUILD_TYPE=Release -DCMAKE_CXX_FLAGS="-O3 -g -Wl,-z,norelro -Wl,--hash-style=gnu -Wl,-z,noseparate-code -ffunction-sections -fdata-sections -Wl,--gc-sections -Wl,--export-dynamic" -DBeamMP-Server_ENABLE_LTO=ON
+1 -1
View File
@@ -4,4 +4,4 @@ set -ex
./vcpkg/bootstrap-vcpkg.sh
cmake . -B bin $1 -DCMAKE_BUILD_TYPE=Release -DCMAKE_CXX_FLAGS="-O3 -g -Wl,-z,norelro -Wl,--hash-style=gnu -Wl,-z,noseparate-code -ffunction-sections -fdata-sections -Wl,--gc-sections" -DBeamMP-Server_ENABLE_LTO=ON
cmake . -B bin $1 -DCMAKE_BUILD_TYPE=Release -DCMAKE_CXX_FLAGS="-O3 -g -Wl,-z,norelro -Wl,--hash-style=gnu -Wl,-z,noseparate-code -ffunction-sections -fdata-sections -Wl,--gc-sections -Wl,--export-dynamic" -DBeamMP-Server_ENABLE_LTO=ON
+1 -1
View File
@@ -4,4 +4,4 @@ set -ex
./vcpkg/bootstrap-vcpkg.sh
cmake . -B bin $1 -DCMAKE_BUILD_TYPE=Release -DCMAKE_CXX_FLAGS="-O3 -g -Wl,-z,norelro -Wl,--hash-style=gnu -Wl,-z,noseparate-code -ffunction-sections -fdata-sections -Wl,--gc-sections" -DBeamMP-Server_ENABLE_LTO=ON
cmake . -B bin $1 -DCMAKE_BUILD_TYPE=Release -DCMAKE_CXX_FLAGS="-O3 -g -Wl,-z,norelro -Wl,--hash-style=gnu -Wl,-z,noseparate-code -ffunction-sections -fdata-sections -Wl,--gc-sections -Wl,--export-dynamic" -DBeamMP-Server_ENABLE_LTO=ON
View File
+1 -1
View File
@@ -4,4 +4,4 @@ set -ex
./vcpkg/bootstrap-vcpkg.sh
cmake . -B bin $1 -DCMAKE_BUILD_TYPE=Release -DCMAKE_CXX_FLAGS="-O3 -g -Wl,-z,norelro -Wl,--hash-style=gnu -Wl,-z,noseparate-code -ffunction-sections -fdata-sections -Wl,--gc-sections" -DBeamMP-Server_ENABLE_LTO=ON
cmake . -B bin $1 -DCMAKE_BUILD_TYPE=Release -DCMAKE_CXX_FLAGS="-O3 -g -Wl,-z,norelro -Wl,--hash-style=gnu -Wl,-z,noseparate-code -ffunction-sections -fdata-sections -Wl,--gc-sections -Wl,--export-dynamic" -DBeamMP-Server_ENABLE_LTO=ON
+7
View File
@@ -0,0 +1,7 @@
#!/bin/bash
set -ex
apt-get update -y
apt-get install -y liblua5.3-0 liblua5.3-dev curl zip unzip tar cmake make git g++ ninja-build
+7
View File
@@ -0,0 +1,7 @@
#!/bin/bash
set -ex
git config --global --add safe.directory $(pwd)
git config --global --add safe.directory $(pwd)/vcpkg
git config --global --add safe.directory $(pwd)/deps/commandline
+7
View File
@@ -0,0 +1,7 @@
#!/bin/bash
set -ex
./vcpkg/bootstrap-vcpkg.sh
cmake . -B bin $1 -DCMAKE_BUILD_TYPE=Release -DCMAKE_CXX_FLAGS="-O3 -g -Wl,-z,norelro -Wl,--hash-style=gnu -Wl,-z,noseparate-code -ffunction-sections -fdata-sections -Wl,--gc-sections -Wl,--export-dynamic" -DBeamMP-Server_ENABLE_LTO=ON
+5
View File
@@ -0,0 +1,5 @@
#!/bin/bash
set -ex
cmake --build bin --parallel -t BeamMP-Server-tests
+10
View File
@@ -0,0 +1,10 @@
#!/bin/bash
set -ex
cmake --build bin --parallel -t BeamMP-Server
objcopy --only-keep-debug bin/BeamMP-Server bin/BeamMP-Server.debug
objcopy --add-gnu-debuglink bin/BeamMP-Server bin/BeamMP-Server.debug
strip -s bin/BeamMP-Server
+8
View File
@@ -0,0 +1,8 @@
#!/bin/bash
set -ex
apt-get update -y
apt-get install -y liblua5.3-0 curl
+23
View File
@@ -0,0 +1,23 @@
#include "ChronoWrapper.h"
#include "Common.h"
#include <regex>
std::chrono::high_resolution_clock::duration ChronoWrapper::TimeFromStringWithLiteral(const std::string& time_str) {
// const std::regex time_regex(R"((\d+\.{0,1}\d*)(min|ms|us|ns|[dhs]))"); //i.e one of: "25ns, 6us, 256ms, 2s, 13min, 69h, 356d" will get matched (only available in newer C++ versions)
const std::regex time_regex(R"((\d+\.{0,1}\d*)(min|[dhs]))"); // i.e one of: "2.01s, 13min, 69h, 356.69d" will get matched
std::smatch match;
float time_value;
if (!std::regex_search(time_str, match, time_regex))
return std::chrono::nanoseconds(0);
time_value = stof(match.str(1));
if (match.str(2) == "d") {
return std::chrono::seconds((uint64_t)(time_value * 86400)); // 86400 seconds in a day
} else if (match.str(2) == "h") {
return std::chrono::seconds((uint64_t)(time_value * 3600)); // 3600 seconds in an hour
} else if (match.str(2) == "min") {
return std::chrono::seconds((uint64_t)(time_value * 60));
} else if (match.str(2) == "s") {
return std::chrono::seconds((uint64_t)time_value);
}
return std::chrono::nanoseconds(0);
}
+184
View File
@@ -0,0 +1,184 @@
// BeamMP, the BeamNG.drive multiplayer mod.
// Copyright (C) 2024 BeamMP Ltd., BeamMP team and contributors.
//
// BeamMP Ltd. can be contacted by electronic mail via contact@beammp.com.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published
// by the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
#include "Client.h"
#include "CustomAssert.h"
#include "TServer.h"
#include <memory>
#include <optional>
void TClient::DeleteCar(int Ident) {
// TODO: Send delete packets
std::unique_lock lock(mVehicleDataMutex);
auto iter = std::find_if(mVehicleData.begin(), mVehicleData.end(), [&](auto& elem) {
return Ident == elem.ID();
});
if (iter != mVehicleData.end()) {
mVehicleData.erase(iter);
} else {
beammp_debug("tried to erase a vehicle that doesn't exist (not an error)");
}
}
void TClient::ClearCars() {
std::unique_lock lock(mVehicleDataMutex);
mVehicleData.clear();
}
int TClient::GetOpenCarID() const {
int OpenID = 0;
bool found;
std::unique_lock lock(mVehicleDataMutex);
do {
found = true;
for (auto& v : mVehicleData) {
if (v.ID() == OpenID) {
OpenID++;
found = false;
}
}
} while (!found);
return OpenID;
}
void TClient::AddNewCar(int Ident, const nlohmann::json& Data) {
std::unique_lock lock(mVehicleDataMutex);
mVehicleData.emplace_back(Ident, Data);
}
TClient::TVehicleDataLockPair TClient::GetAllCars() {
return { &mVehicleData, std::unique_lock(mVehicleDataMutex) };
}
std::string TClient::GetCarPositionRaw(int Ident) {
std::unique_lock lock(mVehiclePositionMutex);
try {
return mVehiclePosition.at(size_t(Ident));
} catch (const std::out_of_range& oor) {
beammp_debugf("Failed to get vehicle position for {}: {}", Ident, oor.what());
return "";
}
}
void TClient::Disconnect(std::string_view Reason) {
beammp_debugf("Disconnecting client {} for reason: {}", GetID(), Reason);
boost::system::error_code ec;
if (mSocket.is_open()) {
mSocket.shutdown(socket_base::shutdown_both, ec);
if (ec) {
beammp_debugf("Failed to shutdown client socket: {}", ec.message());
}
mSocket.close(ec);
if (ec) {
beammp_debugf("Failed to close client socket: {}", ec.message());
}
} else {
beammp_debug("Socket is already closed.");
}
}
void TClient::SetCarPosition(int Ident, const std::string& Data) {
std::unique_lock lock(mVehiclePositionMutex);
mVehiclePosition[size_t(Ident)] = Data;
}
nlohmann::json TClient::GetCarData(int Ident) {
{ // lock
std::unique_lock lock(mVehicleDataMutex);
for (auto& v : mVehicleData) {
if (v.ID() == Ident) {
return v.Data();
}
}
} // unlock
DeleteCar(Ident);
return nlohmann::detail::value_t::null;
}
void TClient::SetCarData(int Ident, const nlohmann::json& Data) {
{ // lock
std::unique_lock lock(mVehicleDataMutex);
for (auto& v : mVehicleData) {
if (v.ID() == Ident) {
v.SetData(Data);
return;
}
}
} // unlock
DeleteCar(Ident);
}
int TClient::GetCarCount() const {
// mVechileData holds both unicycle and cars which both count towards the maximum car count
// spawning a unicycle meant reaching the max, hence being unable to spawn car. this dirty fixes the problem for now.
std::unique_lock lock(mVehicleDataMutex);
for (auto& v : mVehicleData) {
if (v.ID() == mUnicycleID) {
return int(mVehicleData.size() - 1);
}
}
return int(mVehicleData.size());
}
TServer& TClient::Server() const {
return mServer;
}
void TClient::EnqueuePacket(const std::vector<uint8_t>& Packet) {
std::unique_lock Lock(mMissedPacketsMutex);
mPacketsSync.push(Packet);
}
TClient::TClient(TServer& Server, ip::tcp::socket&& Socket)
: mServer(Server)
, mSocket(std::move(Socket))
, mLastPingTime(std::chrono::high_resolution_clock::now()) {
}
TClient::~TClient() {
beammp_debugf("client destroyed: {} ('{}')", this->GetID(), this->GetName());
}
void TClient::UpdatePingTime() {
mLastPingTime = std::chrono::high_resolution_clock::now();
}
int TClient::SecondsSinceLastPing() {
auto seconds = std::chrono::duration_cast<std::chrono::seconds>(
std::chrono::high_resolution_clock::now() - mLastPingTime)
.count();
return int(seconds);
}
std::optional<std::weak_ptr<TClient>> GetClient(TServer& Server, int ID) {
std::optional<std::weak_ptr<TClient>> MaybeClient { std::nullopt };
Server.ForEachClient([&](std::weak_ptr<TClient> CPtr) -> bool {
ReadLock Lock(Server.GetClientMutex());
if (!CPtr.expired()) {
auto C = CPtr.lock();
if (C->GetID() == ID) {
MaybeClient = CPtr;
return false;
}
} else {
beammp_debugf("Found an expired client while looking for id {}", ID);
}
return true;
});
return MaybeClient;
}
+82 -11
View File
@@ -22,7 +22,9 @@
#include "TConsole.h"
#include <array>
#include <charconv>
#include <chrono>
#include <fmt/core.h>
#include <fstream>
#include <iostream>
#include <map>
#include <regex>
@@ -33,8 +35,6 @@
#include "CustomAssert.h"
#include "Http.h"
Application::TSettings Application::Settings = {};
void Application::RegisterShutdownHandler(const TShutdownHandler& Handler) {
std::unique_lock Lock(mShutdownHandlersMutex);
if (Handler) {
@@ -215,14 +215,14 @@ void Application::CheckForUpdates() {
// checks current version against latest version
std::regex VersionRegex { R"(\d+\.\d+\.\d+\n*)" };
for (const auto& url : GetBackendUrlsInOrder()) {
auto Response = Http::GET(url, 443, "/v/s");
auto Response = Http::GET(url + "/v/s");
bool Matches = std::regex_match(Response, VersionRegex);
if (Matches) {
auto MyVersion = ServerVersion();
auto RemoteVersion = Version(VersionStrToInts(Response));
if (IsOutdated(MyVersion, RemoteVersion)) {
std::string RealVersionString = std::string("v") + RemoteVersion.AsString();
const std::string DefaultUpdateMsg = "NEW VERSION IS OUT! Please update to the new version ({}) of the BeamMP-Server! Download it here: https://beammp.com/! For a guide on how to update, visit: https://wiki.beammp.com/en/home/server-maintenance#updating-the-server";
const std::string DefaultUpdateMsg = "NEW VERSION IS OUT! Please update to the new version ({}) of the BeamMP-Server! Download it here: https://beammp.com/! For a guide on how to update, visit: https://docs.beammp.com/server/server-maintenance/#updating-the-server";
auto UpdateMsg = Env::Get(Env::Key::PROVIDER_UPDATE_MESSAGE).value_or(DefaultUpdateMsg);
UpdateMsg = fmt::vformat(std::string_view(UpdateMsg), fmt::make_format_args(RealVersionString));
beammp_warnf("{}{}{}", ANSI_YELLOW_BOLD, UpdateMsg, ANSI_RESET);
@@ -256,7 +256,7 @@ static std::mutex ThreadNameMapMutex {};
std::string ThreadName(bool DebugModeOverride) {
auto Lock = std::unique_lock(ThreadNameMapMutex);
if (DebugModeOverride || Application::Settings.DebugModeEnabled) {
if (DebugModeOverride || Application::Settings.getAsBool(Settings::Key::General_Debug)) {
auto id = std::this_thread::get_id();
if (threadNameMap.find(id) != threadNameMap.end()) {
// found
@@ -268,21 +268,21 @@ std::string ThreadName(bool DebugModeOverride) {
TEST_CASE("ThreadName") {
RegisterThread("MyThread");
auto OrigDebug = Application::Settings.DebugModeEnabled;
auto OrigDebug = Application::Settings.getAsBool(Settings::Key::General_Debug);
// ThreadName adds a space at the end, legacy but we need it still
SUBCASE("Debug mode enabled") {
Application::Settings.DebugModeEnabled = true;
Application::Settings.set(Settings::Key::General_Debug, true);
CHECK(ThreadName(true) == "MyThread ");
CHECK(ThreadName(false) == "MyThread ");
}
SUBCASE("Debug mode disabled") {
Application::Settings.DebugModeEnabled = false;
Application::Settings.set(Settings::Key::General_Debug, false);
CHECK(ThreadName(true) == "MyThread ");
CHECK(ThreadName(false) == "");
}
// cleanup
Application::Settings.DebugModeEnabled = OrigDebug;
Application::Settings.set(Settings::Key::General_Debug, OrigDebug);
}
void RegisterThread(const std::string& str) {
@@ -296,7 +296,7 @@ void RegisterThread(const std::string& str) {
#elif defined(BEAMMP_FREEBSD)
ThreadId = std::to_string(getpid());
#endif
if (Application::Settings.DebugModeEnabled) {
if (Application::Settings.getAsBool(Settings::Key::General_Debug)) {
std::ofstream ThreadFile(".Threads.log", std::ios::app);
ThreadFile << ("Thread \"" + str + "\" is TID " + ThreadId) << std::endl;
}
@@ -329,7 +329,7 @@ TEST_CASE("Version::AsString") {
}
void LogChatMessage(const std::string& name, int id, const std::string& msg) {
if (Application::Settings.LogChat) {
if (Application::Settings.getAsBool(Settings::Key::General_LogChat)) {
std::stringstream ss;
ss << ThreadName();
ss << "[CHAT] ";
@@ -384,3 +384,74 @@ void SplitString(const std::string& str, const char delim, std::vector<std::stri
out.push_back(str.substr(start, end - start));
}
}
std::string LowerString(std::string str) {
std::ranges::transform(str, str.begin(), ::tolower);
return str;
}
static constexpr size_t STARTING_MAX_DECOMPRESSION_BUFFER_SIZE = 15 * 1024 * 1024;
static constexpr size_t MAX_DECOMPRESSION_BUFFER_SIZE = 30 * 1024 * 1024;
std::vector<uint8_t> DeComp(std::span<const uint8_t> input) {
beammp_debugf("got {} bytes of input data", input.size());
// start with a decompression buffer of 5x the input size, clamped to a maximum of 15 MB.
// this buffer can and will grow, but we don't want to start it too large. A 5x compression ratio
// is pretty optimistic.
std::vector<uint8_t> output_buffer(std::min<size_t>(input.size() * 5, STARTING_MAX_DECOMPRESSION_BUFFER_SIZE));
uLongf output_size = output_buffer.size();
while (true) {
int res = uncompress(
reinterpret_cast<Bytef*>(output_buffer.data()),
&output_size,
reinterpret_cast<const Bytef*>(input.data()),
static_cast<uLongf>(input.size()));
if (res == Z_BUF_ERROR) {
// We assume that a reasonable maximum size for decompressed packets exists. We want to avoid
// a client effectively "zip bombing" us by sending a lot of small packets which decompress
// into huge data.
// If this limit were to be an issue, this could be made configurable, however clients have a similar
// limit. For that reason, we just reject packets which decompress into too much data.
if (output_buffer.size() >= MAX_DECOMPRESSION_BUFFER_SIZE) {
throw std::runtime_error(fmt::format("decompressed packet size of {} bytes exceeded", MAX_DECOMPRESSION_BUFFER_SIZE));
}
// if decompression fails, we double the buffer size (up to the allowed limit) and try again
output_buffer.resize(std::max<size_t>(output_buffer.size() * 2, MAX_DECOMPRESSION_BUFFER_SIZE));
beammp_warnf("zlib uncompress() failed, trying with a larger buffer size of {}", output_buffer.size());
output_size = output_buffer.size();
} else if (res != Z_OK) {
beammp_error("zlib uncompress() failed: " + std::to_string(res));
if (res == Z_DATA_ERROR) {
throw InvalidDataError {};
} else {
throw std::runtime_error("zlib uncompress() failed");
}
} else if (res == Z_OK) {
break;
}
}
output_buffer.resize(output_size);
return output_buffer;
}
std::vector<uint8_t> Comp(std::span<const uint8_t> input) {
auto max_size = compressBound(input.size());
std::vector<uint8_t> output(max_size);
uLongf output_size = output.size();
int res = compress(
reinterpret_cast<Bytef*>(output.data()),
&output_size,
reinterpret_cast<const Bytef*>(input.data()),
static_cast<uLongf>(input.size()));
if (res != Z_OK) {
beammp_error("zlib compress() failed: " + std::to_string(res));
throw std::runtime_error("zlib compress() failed");
}
beammp_debug("zlib compressed " + std::to_string(input.size()) + " B to " + std::to_string(output_size) + " B");
output.resize(output_size);
return output;
}
+1 -1
View File
@@ -61,7 +61,7 @@ TEST_CASE("init and reset termios") {
CHECK_EQ(current.c_lflag, original.c_lflag);
#ifndef BEAMMP_FREEBSD // The 'c_line' attribute seems to only exist on Linux, so we need to omit it on other platforms
CHECK_EQ(current.c_line, original.c_line);
#endif
#endif
CHECK_EQ(current.c_oflag, original.c_oflag);
CHECK_EQ(current.c_ospeed, original.c_ospeed);
}
+9
View File
@@ -33,6 +33,15 @@ std::string_view Env::ToString(Env::Key key) {
case Key::PROVIDER_UPDATE_MESSAGE:
return "BEAMMP_PROVIDER_UPDATE_MESSAGE";
break;
case Key::PROVIDER_DISABLE_CONFIG:
return "BEAMMP_PROVIDER_DISABLE_CONFIG";
break;
case Key::PROVIDER_PORT_ENV:
return "BEAMMP_PROVIDER_PORT_ENV";
break;
case Key::PROVIDER_IP_ENV:
return "BEAMMP_PROVIDER_IP_ENV";
break;
}
return "";
}
-101
View File
@@ -1,101 +0,0 @@
#include "FileWatcher.h"
#include "Common.h"
#include <chrono>
#include <filesystem>
/// @file
/// This file holds the FileWatcher implementation.
FileWatcher::FileWatcher(unsigned int seconds)
: m_seconds(boost::posix_time::seconds(seconds))
, m_timer(boost::asio::deadline_timer(m_io, *m_seconds)) {
m_timer->async_wait([this](const auto& err) { on_tick(err); });
m_thread = boost::scoped_thread<> { &FileWatcher::thread_main, this };
}
FileWatcher::~FileWatcher() {
m_work_guard.reset();
*m_shutdown = true;
}
void FileWatcher::watch_file(const std::filesystem::path& path) {
m_files->insert(path);
}
void FileWatcher::watch_files_in(const std::filesystem::path& path) {
m_dirs->insert(path);
}
void FileWatcher::thread_main() {
while (!*m_shutdown) {
m_io.run_for(std::chrono::seconds(1));
}
}
void FileWatcher::on_tick(const boost::system::error_code& err) {
auto timer = m_timer.synchronize();
// set up timer so that the operations after this don't impact the accuracy of the
// timing
timer->expires_at(timer->expires_at() + *m_seconds);
if (err) {
beammp_errorf("FileWatcher encountered error: {}", err.message());
// TODO: Should any further action be taken?
} else {
try {
check_files();
} catch (const std::exception& e) {
beammp_errorf("FileWatcher exception while checking files: {}", e.what());
}
try {
check_directories();
} catch (const std::exception& e) {
beammp_errorf("FileWatcher exception while checking directories: {}", e.what());
}
}
// finally start the timer again, deadline has already been set at the beginning
// of this function
timer->async_wait([this](const auto& err) { on_tick(err); });
}
void FileWatcher::check_files() {
auto files = m_files.synchronize();
for (const auto& file : *files) {
check_file(file);
// TODO: add deleted/created watches
}
}
void FileWatcher::check_directories() {
auto directories = m_dirs.synchronize();
for (const auto& dir : *directories) {
if (std::filesystem::exists(dir)) {
for (const auto& entry : std::filesystem::recursive_directory_iterator(dir, std::filesystem::directory_options::follow_directory_symlink | std::filesystem::directory_options::skip_permission_denied)) {
if (entry.is_regular_file() || entry.is_symlink()) {
check_file(entry.path());
}
}
}
// TODO: add deleted/created watches
}
}
void FileWatcher::check_file(const std::filesystem::path& file) {
if (std::filesystem::exists(file)) {
auto real_file = file;
if (std::filesystem::is_symlink(file)) {
real_file = std::filesystem::read_symlink(file);
}
auto time = std::filesystem::last_write_time(real_file);
if (!m_file_mod_times.contains(file)) {
m_file_mod_times.insert_or_assign(file, time);
} else {
if (m_file_mod_times.at(file) != time) {
beammp_tracef("File changed: {}", file);
sig_file_changed(file);;
m_file_mod_times.at(file) = time;
}
}
}
}
+77 -33
View File
@@ -18,6 +18,7 @@
#include "Http.h"
#include "Client.h"
#include "Common.h"
#include "CustomAssert.h"
#include "LuaAPI.h"
@@ -27,41 +28,89 @@
#include <random>
#include <stdexcept>
// TODO: Add sentry error handling back
using json = nlohmann::json;
std::string Http::GET(const std::string& host, int port, const std::string& target, unsigned int* status) {
static thread_local httplib::SSLClient client(host, port);
client.enable_server_certificate_verification(false);
client.set_address_family(AF_INET);
auto res = client.Get(target.c_str());
if (res) {
if (status) {
*status = res->status;
}
return res->body;
} else {
return Http::ErrorString;
}
static size_t CurlWriteCallback(void* contents, size_t size, size_t nmemb, void* userp) {
std::string* Result = reinterpret_cast<std::string*>(userp);
std::string NewContents(reinterpret_cast<char*>(contents), size * nmemb);
*Result += NewContents;
return size * nmemb;
}
std::string Http::POST(const std::string& host, int port, const std::string& target, const std::string& body, const std::string& ContentType, unsigned int* status, const httplib::Headers& headers) {
static thread_local httplib::SSLClient client(host, port);
client.set_read_timeout(std::chrono::seconds(10));
beammp_assert(client.is_valid());
client.enable_server_certificate_verification(false);
client.set_address_family(AF_INET);
auto res = client.Post(target.c_str(), headers, body.c_str(), body.size(), ContentType.c_str());
if (res) {
if (status) {
*status = res->status;
std::string Http::GET(const std::string& url, unsigned int* status) {
std::string Ret;
static thread_local CURL* curl = curl_easy_init();
if (curl) {
CURLcode res;
char errbuf[CURL_ERROR_SIZE];
curl_easy_setopt(curl, CURLOPT_URL, url.c_str());
curl_easy_setopt(curl, CURLOPT_IPRESOLVE, CURL_IPRESOLVE_V4);
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, CurlWriteCallback);
curl_easy_setopt(curl, CURLOPT_WRITEDATA, (void*)&Ret);
curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT, 10); // seconds
curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L);
curl_easy_setopt(curl, CURLOPT_ERRORBUFFER, errbuf);
errbuf[0] = 0;
res = curl_easy_perform(curl);
if (res != CURLE_OK) {
beammp_error("GET to " + url + " failed: " + std::string(curl_easy_strerror(res)));
beammp_error("Curl error: " + std::string(errbuf));
return Http::ErrorString;
}
return res->body;
if (status) {
curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, status);
}
} else {
beammp_debug("POST failed: " + httplib::to_string(res.error()));
beammp_error("Curl easy init failed");
return Http::ErrorString;
}
return Ret;
}
std::string Http::POST(const std::string& url, const std::string& body, const std::string& ContentType, unsigned int* status, const std::map<std::string, std::string>& headers) {
std::string Ret;
static thread_local CURL* curl = curl_easy_init();
if (curl) {
CURLcode res;
char errbuf[CURL_ERROR_SIZE];
curl_easy_setopt(curl, CURLOPT_URL, url.c_str());
curl_easy_setopt(curl, CURLOPT_IPRESOLVE, CURL_IPRESOLVE_V4);
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, CurlWriteCallback);
curl_easy_setopt(curl, CURLOPT_WRITEDATA, (void*)&Ret);
curl_easy_setopt(curl, CURLOPT_POST, 1);
curl_easy_setopt(curl, CURLOPT_POSTFIELDS, body.c_str());
curl_easy_setopt(curl, CURLOPT_POSTFIELDSIZE, body.size());
struct curl_slist* list = nullptr;
list = curl_slist_append(list, ("Content-Type: " + ContentType).c_str());
for (auto [header, value] : headers) {
list = curl_slist_append(list, (header + ": " + value).c_str());
}
curl_easy_setopt(curl, CURLOPT_HTTPHEADER, list);
curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT, 10); // seconds
curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L);
curl_easy_setopt(curl, CURLOPT_ERRORBUFFER, errbuf);
errbuf[0] = 0;
res = curl_easy_perform(curl);
curl_slist_free_all(list);
if (res != CURLE_OK) {
beammp_error("POST to " + url + " failed: " + std::string(curl_easy_strerror(res)));
beammp_error("Curl error: " + std::string(errbuf));
return Http::ErrorString;
}
if (status) {
curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, status);
}
} else {
beammp_error("Curl easy init failed");
return Http::ErrorString;
}
return Ret;
}
// RFC 2616, RFC 7231
@@ -171,7 +220,6 @@ Http::Server::THttpServerInstance::THttpServerInstance() {
}
void Http::Server::THttpServerInstance::operator()() try {
beammp_info("HTTP(S) Server started on port " + std::to_string(Application::Settings.HTTPServerPort));
std::unique_ptr<httplib::Server> HttpLibServerInstance;
HttpLibServerInstance = std::make_unique<httplib::Server>();
// todo: make this IP agnostic so people can set their own IP
@@ -198,7 +246,7 @@ void Http::Server::THttpServerInstance::operator()() try {
}
}
res.set_content(
nlohmann::json {
json {
{ "ok", SystemsBad == 0 },
}
.dump(),
@@ -213,10 +261,6 @@ void Http::Server::THttpServerInstance::operator()() try {
beammp_debug("Http Server: " + Req.method + " " + Req.target + " -> " + std::to_string(Res.status));
});
Application::SetSubsystemStatus("HTTPServer", Application::Status::Good);
auto ret = HttpLibServerInstance->listen(Application::Settings.HTTPServerIP.c_str(), Application::Settings.HTTPServerPort);
if (!ret) {
beammp_error("Failed to start http server (failed to listen). Please ensure the http server is configured properly in the ServerConfig.toml, or turn it off if you don't need it.");
}
} catch (const std::exception& e) {
beammp_error("Failed to start http server. Please ensure the http server is configured properly in the ServerConfig.toml, or turn it off if you don't need it. Error: " + std::string(e.what()));
}
+338 -233
View File
@@ -17,17 +17,94 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>.
#include "LuaAPI.h"
#include "Client.h"
#include "Common.h"
#include "CustomAssert.h"
#include "Value.h"
#include "Settings.h"
#include "TLuaEngine.h"
#include <boost/json/serialize.hpp>
#include <nlohmann/json.hpp>
#include <sol/types.hpp>
#define SOL_ALL_SAFETIES_ON 1
#include <sol/sol.hpp>
std::string LuaAPI::LuaToString(const sol::object Value, size_t Indent, bool QuoteStrings) {
if (Indent > 80) {
return "[[possible recursion, refusing to keep printing]]";
}
switch (Value.get_type()) {
case sol::type::userdata: {
std::stringstream ss;
ss << "[[userdata: " << Value.as<sol::userdata>().pointer() << "]]";
return ss.str();
}
case sol::type::thread: {
std::stringstream ss;
ss << "[[thread: " << Value.as<sol::thread>().pointer() << "]] {"
<< "\n";
for (size_t i = 0; i < Indent; ++i) {
ss << "\t";
}
ss << "status: " << std::to_string(int(Value.as<sol::thread>().status())) << "\n}";
return ss.str();
}
case sol::type::lightuserdata: {
std::stringstream ss;
ss << "[[lightuserdata: " << Value.as<sol::lightuserdata>().pointer() << "]]";
return ss.str();
}
case sol::type::string:
if (QuoteStrings) {
return "\"" + Value.as<std::string>() + "\"";
} else {
return Value.as<std::string>();
}
case sol::type::number: {
std::stringstream ss;
if (Value.is<int>()) {
ss << Value.as<int>();
} else {
ss << Value.as<float>();
}
return ss.str();
}
case sol::type::lua_nil:
case sol::type::none:
return "<nil>";
case sol::type::boolean:
return Value.as<bool>() ? "true" : "false";
case sol::type::table: {
std::stringstream Result;
auto Table = Value.as<sol::table>();
Result << "[[table: " << Table.pointer() << "]]: {";
if (!Table.empty()) {
for (const auto& Entry : Table) {
Result << "\n";
for (size_t i = 0; i < Indent; ++i) {
Result << "\t";
}
Result << LuaToString(Entry.first, Indent + 1) << ": " << LuaToString(Entry.second, Indent + 1, true) << ",";
}
Result << "\n";
}
for (size_t i = 0; i < Indent - 1; ++i) {
Result << "\t";
}
Result << "}";
return Result.str();
}
case sol::type::function: {
std::stringstream ss;
ss << "[[function: " << Value.as<sol::function>().pointer() << "]]";
return ss.str();
}
case sol::type::poly:
return "<poly>";
default:
return "<unprintable type>";
}
}
std::string LuaAPI::MP::GetOSName() {
#if WIN32
return "Windows";
@@ -42,6 +119,15 @@ std::tuple<int, int, int> LuaAPI::MP::GetServerVersion() {
return { Application::ServerVersion().major, Application::ServerVersion().minor, Application::ServerVersion().patch };
}
void LuaAPI::Print(sol::variadic_args Args) {
std::string ToPrint = "";
for (const auto& Arg : Args) {
ToPrint += LuaToString(static_cast<const sol::object>(Arg));
ToPrint += "\t";
}
luaprint(ToPrint);
}
TEST_CASE("LuaAPI::MP::GetServerVersion") {
const auto [ma, mi, pa] = LuaAPI::MP::GetServerVersion();
const auto real = Application::ServerVersion();
@@ -51,26 +137,29 @@ TEST_CASE("LuaAPI::MP::GetServerVersion") {
}
static inline std::pair<bool, std::string> InternalTriggerClientEvent(int PlayerID, const std::string& EventName, const std::string& Data) {
throw std::runtime_error(fmt::format("NOT IMPLEMENTED: {}", __func__));
/*
std::string Packet = "E:" + EventName + ":" + Data;
if (PlayerID == -1) {
LuaAPI::MP::Engine->Network().SendToAll(nullptr, StringToVector(Packet), true, true);
return { true, "" };
} else {
auto MaybeClient = GetClient(LuaAPI::MP::Engine->Server(), PlayerID);
if (!MaybeClient) {
beammp_errorff("TriggerClientEvent invalid Player ID '{}'", PlayerID);
return { false, "Invalid Player ID" };
}
auto c = MaybeClient.value();
if (!LuaAPI::MP::Engine->Network().Respond(*c, StringToVector(Packet), true)) {
beammp_errorff("Respond failed, dropping client {}", PlayerID);
LuaAPI::MP::Engine->Network().Disconnect(*c);
return { false, "Respond failed, dropping client" };
}
return { true, "" };
}*/
std::string Packet = "E:" + EventName + ":" + Data;
if (PlayerID == -1) {
LuaAPI::MP::Engine->Network().SendToAll(nullptr, StringToVector(Packet), true, true);
return { true, "" };
} else {
auto MaybeClient = GetClient(LuaAPI::MP::Engine->Server(), PlayerID);
if (!MaybeClient || MaybeClient.value().expired()) {
beammp_lua_errorf("TriggerClientEvent invalid Player ID '{}'", PlayerID);
return { false, "Invalid Player ID" };
}
auto c = MaybeClient.value().lock();
if (!c->IsSyncing() && !c->IsSynced()) {
return { false, "Player hasn't joined yet" };
}
if (!LuaAPI::MP::Engine->Network().Respond(*c, StringToVector(Packet), true)) {
beammp_lua_errorf("Respond failed, dropping client {}", PlayerID);
LuaAPI::MP::Engine->Network().ClientKick(*c, "Disconnected after failing to receive packets");
return { false, "Respond failed, dropping client" };
}
return { true, "" };
}
}
std::pair<bool, std::string> LuaAPI::MP::TriggerClientEvent(int PlayerID, const std::string& EventName, const sol::object& DataObj) {
@@ -79,22 +168,17 @@ std::pair<bool, std::string> LuaAPI::MP::TriggerClientEvent(int PlayerID, const
}
std::pair<bool, std::string> LuaAPI::MP::DropPlayer(int ID, std::optional<std::string> MaybeReason) {
throw std::runtime_error(fmt::format("NOT IMPLEMENTED: {}", __func__));
/*
auto MaybeClient = GetClient(Engine->Server(), ID);
if (!MaybeClient) {
beammp_errorff("Tried to drop client with id {}, who doesn't exist", ID);
if (!MaybeClient || MaybeClient.value().expired()) {
beammp_lua_errorf("Tried to drop client with id {}, who doesn't exist", ID);
return { false, "Player does not exist" };
}
auto c = MaybeClient.value();
auto c = MaybeClient.value().lock();
LuaAPI::MP::Engine->Network().ClientKick(*c, MaybeReason.value_or("No reason"));
return { true, "" };
*/
}
std::pair<bool, std::string> LuaAPI::MP::SendChatMessage(int ID, const std::string& Message) {
throw std::runtime_error(fmt::format("NOT IMPLEMENTED: {}", __func__));
/*
std::pair<bool, std::string> Result;
std::string Packet = "C:Server: " + Message;
if (ID == -1) {
@@ -103,45 +187,115 @@ std::pair<bool, std::string> LuaAPI::MP::SendChatMessage(int ID, const std::stri
Result.first = true;
} else {
auto MaybeClient = GetClient(Engine->Server(), ID);
if (MaybeClient) {
auto c = MaybeClient.value();
if (!c->IsSynced) {
if (MaybeClient && !MaybeClient.value().expired()) {
auto c = MaybeClient.value().lock();
if (!c->IsSynced()) {
Result.first = false;
Result.second = "Player still syncing data";
return Result;
}
LogChatMessage("<Server> (to \"" + c->Name.get() + "\")", -1, Message);
LogChatMessage("<Server> (to \"" + c->GetName() + "\")", -1, Message);
if (!Engine->Network().Respond(*c, StringToVector(Packet), true)) {
beammp_errorf("Failed to send chat message back to sender (id {}) - did the sender disconnect?", ID);
beammp_infof("Disconnecting client {} for failure to receive a chat message (TCP disconnect)", c->Name.get());
Engine->Network().Disconnect(c);
// TODO: should we return an error here?
}
Result.first = true;
} else {
beammp_errorf("SendChatMessage invalid argument [1] invalid ID");
beammp_lua_error("SendChatMessage invalid argument [1] invalid ID");
Result.first = false;
Result.second = "Invalid Player ID";
}
return Result;
}
return Result;
}
std::pair<bool, std::string> LuaAPI::MP::SendNotification(int ID, const std::string& Message, const std::string& Icon, const std::string& Category) {
std::pair<bool, std::string> Result;
std::string Packet = "n" + Category + ":" + Icon + ":" + Message;
if (ID == -1) {
Engine->Network().SendToAll(nullptr, StringToVector(Packet), true, true);
Result.first = true;
} else {
auto MaybeClient = GetClient(Engine->Server(), ID);
if (MaybeClient) {
auto c = MaybeClient.value().lock();
if (!c->IsSynced()) {
Result.first = false;
Result.second = "Player is not synced yet";
return Result;
}
if (!Engine->Network().Respond(*c, StringToVector(Packet), true)) {
beammp_errorf("Failed to send notification to player (id {}) - did the player disconnect?", ID);
Result.first = false;
Result.second = "Failed to send packet";
}
Result.first = true;
} else {
beammp_lua_error("SendNotification invalid argument [1] invalid ID");
Result.first = false;
Result.second = "Invalid Player ID";
}
return Result;
}
return Result;
}
std::pair<bool, std::string> LuaAPI::MP::ConfirmationDialog(int ID, const std::string& Title, const std::string& Body, const sol::table& buttons, const std::string& InteractionID, const bool& warning, const bool& reportToServer, const bool& reportToExtensions) {
std::pair<bool, std::string> Result;
const nlohmann::json PacketBody = {
{ "title", Title },
{ "body", Body },
{ "buttons", nlohmann::json::parse(JsonEncode(buttons), nullptr, false) },
{ "interactionID", InteractionID },
{ "class", warning ? "experimental" : "" },
{ "reportToServer", reportToServer },
{ "reportToExtensions", reportToExtensions }
};
std::string Packet = "D" + PacketBody.dump();
if (ID == -1) {
Engine->Network().SendToAll(nullptr, StringToVector(Packet), true, true);
Result.first = true;
} else {
auto MaybeClient = GetClient(Engine->Server(), ID);
if (MaybeClient) {
auto c = MaybeClient.value().lock();
if (!c->IsSynced()) {
Result.first = false;
Result.second = "Player is not synced yet";
return Result;
}
if (!Engine->Network().Respond(*c, StringToVector(Packet), true)) {
beammp_errorf("Failed to send confirmation dialog to player (id {}) - did the player disconnect?", ID);
Result.first = false;
Result.second = "Failed to send packet";
}
Result.first = true;
} else {
beammp_lua_error("ConfirmationDialog invalid argument [1] invalid ID");
Result.first = false;
Result.second = "Invalid Player ID";
}
return Result;
}
return Result;
*/
}
std::pair<bool, std::string> LuaAPI::MP::RemoveVehicle(int PID, int VID) {
throw std::runtime_error(fmt::format("NOT IMPLEMENTED: {}", __func__));
/*
std::pair<bool, std::string> Result;
auto MaybeClient = GetClient(Engine->Server(), PID);
if (!MaybeClient) {
beammp_errorf("RemoveVehicle invalid Player ID");
if (!MaybeClient || MaybeClient.value().expired()) {
beammp_lua_error("RemoveVehicle invalid Player ID");
Result.first = false;
Result.second = "Invalid Player ID";
return Result;
}
auto c = MaybeClient.value();
if (!c->GetCarData(VID).empty()) {
auto c = MaybeClient.value().lock();
if (c->GetCarData(VID) != nlohmann::detail::value_t::null) {
std::string Destroy = "Od:" + std::to_string(PID) + "-" + std::to_string(VID);
LuaAPI::MP::Engine->ReportErrors(LuaAPI::MP::Engine->TriggerEvent("onVehicleDeleted", "", PID, VID));
Engine->Network().SendToAll(nullptr, StringToVector(Destroy), true, true);
c->DeleteCar(VID);
Result.first = true;
@@ -150,94 +304,140 @@ std::pair<bool, std::string> LuaAPI::MP::RemoveVehicle(int PID, int VID) {
Result.second = "Vehicle does not exist";
}
return Result;
*/
}
void LuaAPI::MP::Set(int ConfigID, sol::object NewValue) {
switch (ConfigID) {
case 0: // debug
if (NewValue.is<bool>()) {
Application::Settings.DebugModeEnabled = NewValue.as<bool>();
beammp_info(std::string("Set `Debug` to ") + (Application::Settings.DebugModeEnabled ? "true" : "false"));
Application::Settings.set(Settings::Key::General_Debug, NewValue.as<bool>());
beammp_info(std::string("Set `Debug` to ") + (Application::Settings.getAsBool(Settings::Key::General_Debug) ? "true" : "false"));
} else {
beammp_errorf("set invalid argument [2] expected boolean");
beammp_lua_error("set invalid argument [2] expected boolean");
}
break;
case 1: // private
if (NewValue.is<bool>()) {
Application::Settings.Private = NewValue.as<bool>();
beammp_info(std::string("Set `Private` to ") + (Application::Settings.Private ? "true" : "false"));
Application::Settings.set(Settings::Key::General_Private, NewValue.as<bool>());
beammp_info(std::string("Set `Private` to ") + (Application::Settings.getAsBool(Settings::Key::General_Private) ? "true" : "false"));
} else {
beammp_errorf("set invalid argument [2] expected boolean");
beammp_lua_error("set invalid argument [2] expected boolean");
}
break;
case 2: // max cars
if (NewValue.is<int>()) {
Application::Settings.MaxCars = NewValue.as<int>();
beammp_info(std::string("Set `MaxCars` to ") + std::to_string(Application::Settings.MaxCars));
Application::Settings.set(Settings::Key::General_MaxCars, NewValue.as<int>());
beammp_info(std::string("Set `MaxCars` to ") + std::to_string(Application::Settings.getAsInt(Settings::Key::General_MaxCars)));
} else {
beammp_errorf("set invalid argument [2] expected integer");
beammp_lua_error("set invalid argument [2] expected integer");
}
break;
case 3: // max players
if (NewValue.is<int>()) {
Application::Settings.MaxPlayers = NewValue.as<int>();
beammp_info(std::string("Set `MaxPlayers` to ") + std::to_string(Application::Settings.MaxPlayers));
Application::Settings.set(Settings::Key::General_MaxPlayers, NewValue.as<int>());
beammp_info(std::string("Set `MaxPlayers` to ") + std::to_string(Application::Settings.getAsInt(Settings::Key::General_MaxPlayers)));
} else {
beammp_errorf("set invalid argument [2] expected integer");
beammp_lua_error("set invalid argument [2] expected integer");
}
break;
case 4: // Map
if (NewValue.is<std::string>()) {
Application::Settings.MapName = NewValue.as<std::string>();
beammp_info(std::string("Set `Map` to ") + Application::Settings.MapName);
Application::Settings.set(Settings::Key::General_Map, NewValue.as<std::string>());
beammp_info(std::string("Set `Map` to ") + Application::Settings.getAsString(Settings::Key::General_Map));
} else {
beammp_errorf("set invalid argument [2] expected string");
beammp_lua_error("set invalid argument [2] expected string");
}
break;
case 5: // Name
if (NewValue.is<std::string>()) {
Application::Settings.ServerName = NewValue.as<std::string>();
beammp_info(std::string("Set `Name` to ") + Application::Settings.ServerName);
Application::Settings.set(Settings::Key::General_Name, NewValue.as<std::string>());
beammp_info(std::string("Set `Name` to ") + Application::Settings.getAsString(Settings::Key::General_Name));
} else {
beammp_errorf("set invalid argument [2] expected string");
beammp_lua_error("set invalid argument [2] expected string");
}
break;
case 6: // Desc
if (NewValue.is<std::string>()) {
Application::Settings.ServerDesc = NewValue.as<std::string>();
beammp_info(std::string("Set `Description` to ") + Application::Settings.ServerDesc);
Application::Settings.set(Settings::Key::General_Description, NewValue.as<std::string>());
beammp_info(std::string("Set `Description` to ") + Application::Settings.getAsString(Settings::Key::General_Description));
} else {
beammp_errorf("set invalid argument [2] expected string");
beammp_lua_error("set invalid argument [2] expected string");
}
break;
case 7: // Information packet
if (NewValue.is<bool>()) {
Application::Settings.set(Settings::Key::General_InformationPacket, NewValue.as<bool>());
beammp_info(std::string("Set `InformationPacket` to ") + (Application::Settings.getAsBool(Settings::Key::General_InformationPacket) ? "true" : "false"));
} else {
beammp_lua_error("set invalid argument [2] expected boolean");
}
break;
default:
beammp_warn("Invalid config ID \"" + std::to_string(ConfigID) + "\". Use `MP.Settings.*` enum for this.");
break;
}
}
bool LuaAPI::MP::IsPlayerConnected(int ID) {
throw std::runtime_error(fmt::format("NOT IMPLEMENTED: {}", __func__));
/*
auto MaybeClient = GetClient(Engine->Server(), ID);
if (MaybeClient) {
return MaybeClient.value()->IsConnected.get();
} else {
return false;
}*/
TLuaValue LuaAPI::MP::Get(int ConfigID) {
switch (ConfigID) {
case 0: // debug
return Application::Settings.getAsBool(Settings::Key::General_Debug);
case 1: // private
return Application::Settings.getAsBool(Settings::Key::General_Private);
case 2: // max cars
return Application::Settings.getAsInt(Settings::Key::General_MaxCars);
case 3: // max players
return Application::Settings.getAsInt(Settings::Key::General_MaxPlayers);
case 4: // Map
return Application::Settings.getAsString(Settings::Key::General_Map);
case 5: // Name
return Application::Settings.getAsString(Settings::Key::General_Name);
case 6: // Desc
return Application::Settings.getAsString(Settings::Key::General_Description);
case 7: // Information packet
return Application::Settings.getAsBool(Settings::Key::General_InformationPacket);
default:
beammp_warn("Invalid config ID \"" + std::to_string(ConfigID) + "\". Use `MP.Settings.*` enum for this.");
return 0;
}
}
bool LuaAPI::MP::IsPlayerGuest(int ID) {
throw std::runtime_error(fmt::format("NOT IMPLEMENTED: {}", __func__));
/*
void LuaAPI::MP::Sleep(size_t Ms) {
std::this_thread::sleep_for(std::chrono::milliseconds(Ms));
}
bool LuaAPI::MP::IsPlayerConnected(int ID) {
auto MaybeClient = GetClient(Engine->Server(), ID);
if (MaybeClient) {
return MaybeClient.value()->IsGuest.get();
if (MaybeClient && !MaybeClient.value().expired()) {
return MaybeClient.value().lock()->IsUDPConnected();
} else {
return false;
}
*/
}
bool LuaAPI::MP::IsPlayerGuest(int ID) {
auto MaybeClient = GetClient(Engine->Server(), ID);
if (MaybeClient && !MaybeClient.value().expired()) {
return MaybeClient.value().lock()->IsGuest();
} else {
return false;
}
}
void LuaAPI::MP::PrintRaw(sol::variadic_args Args) {
std::string ToPrint = "";
for (const auto& Arg : Args) {
ToPrint += LuaToString(static_cast<const sol::object>(Arg));
ToPrint += "\t";
}
#ifdef DOCTEST_CONFIG_DISABLE
Application::Console().WriteRaw(ToPrint);
#endif
}
int LuaAPI::PanicHandler(lua_State* State) {
beammp_lua_error("PANIC: " + sol::stack::get<std::string>(State, 1));
return 0;
}
template <typename FnT, typename... ArgsT>
@@ -443,7 +643,7 @@ std::string LuaAPI::FS::ConcatPaths(sol::variadic_args Args) {
for (size_t i = 0; i < Args.size(); ++i) {
auto Obj = Args[i];
if (!Obj.is<std::string>()) {
beammp_errorf("FS.Concat called with non-string argument");
beammp_lua_error("FS.Concat called with non-string argument");
return "";
}
Path += Obj.as<std::string>();
@@ -457,7 +657,7 @@ std::string LuaAPI::FS::ConcatPaths(sol::variadic_args Args) {
static void JsonEncodeRecursive(nlohmann::json& json, const sol::object& left, const sol::object& right, bool is_array, size_t depth = 0) {
if (depth > 100) {
beammp_errorf("json serialize will not go deeper than 100 nested tables, internal references assumed, aborted this path");
beammp_lua_error("json serialize will not go deeper than 100 nested tables, internal references assumed, aborted this path");
return;
}
std::string key {};
@@ -471,13 +671,17 @@ static void JsonEncodeRecursive(nlohmann::json& json, const sol::object& left, c
case sol::type::thread:
case sol::type::function:
case sol::type::table:
beammp_errorf("JsonEncode: left side of table field is unexpected type");
beammp_lua_error("JsonEncode: left side of table field is unexpected type");
return;
case sol::type::string:
key = left.as<std::string>();
break;
case sol::type::number:
key = std::to_string(left.as<double>());
if (left.is<int>()) {
key = std::to_string(left.as<int>());
} else {
key = std::to_string(left.as<double>());
}
break;
default:
beammp_assert_not_reachable();
@@ -505,21 +709,30 @@ static void JsonEncodeRecursive(nlohmann::json& json, const sol::object& left, c
case sol::type::string:
value = right.as<std::string>();
break;
case sol::type::number:
value = right.as<double>();
case sol::type::number: {
if (right.is<int>()) {
value = right.as<int>();
} else {
value = right.as<double>();
}
break;
}
case sol::type::function:
beammp_lua_warn("unsure what to do with function in JsonEncode, ignoring");
return;
case sol::type::table: {
bool local_is_array = true;
for (const auto& pair : right.as<sol::table>()) {
if (pair.first.get_type() != sol::type::number) {
local_is_array = false;
if (right.as<sol::table>().empty()) {
value = nlohmann::json::object();
} else {
bool local_is_array = true;
for (const auto& pair : right.as<sol::table>()) {
if (pair.first.get_type() != sol::type::number) {
local_is_array = false;
}
}
for (const auto& pair : right.as<sol::table>()) {
JsonEncodeRecursive(value, pair.first, pair.second, local_is_array, depth + 1);
}
}
for (const auto& pair : right.as<sol::table>()) {
JsonEncodeRecursive(value, pair.first, pair.second, local_is_array, depth + 1);
}
break;
}
@@ -533,33 +746,32 @@ static void JsonEncodeRecursive(nlohmann::json& json, const sol::object& left, c
}
}
static std::string lua_to_json_impl(const sol::object& obj) {
// used as the invalid value provider in sol_obj_to_value.
auto special_stringifier = [](const sol::object& object) -> Result<Value> {
beammp_debugf("Cannot convert from type {} to json, ignoring (using null)", int(object.get_type()));
return { Null {} };
};
auto maybe_val = sol_obj_to_value(obj, special_stringifier);
if (maybe_val) {
auto result = boost::apply_visitor(ValueToJsonVisitor {}, maybe_val.move());
return boost::json::serialize(result);
std::string LuaAPI::MP::JsonEncode(const sol::table& object) {
nlohmann::json json;
// table
if (object.as<sol::table>().empty()) {
json = nlohmann::json::object();
} else {
beammp_errorf("Failed to convert an argument to json: {}", maybe_val.error);
return "";
bool is_array = true;
for (const auto& pair : object.as<sol::table>()) {
if (pair.first.get_type() != sol::type::number) {
is_array = false;
}
}
for (const auto& entry : object) {
JsonEncodeRecursive(json, entry.first, entry.second, is_array);
}
}
return json.dump();
}
std::string LuaAPI::Util::JsonEncode(const sol::object& object) {
return lua_to_json_impl(object);
}
std::string LuaAPI::Util::JsonDiff(const std::string& a, const std::string& b) {
std::string LuaAPI::MP::JsonDiff(const std::string& a, const std::string& b) {
if (!nlohmann::json::accept(a)) {
beammp_error("JsonDiff first argument is not valid json: `" + a + "`");
beammp_lua_error("JsonDiff first argument is not valid json: `" + a + "`");
return "";
}
if (!nlohmann::json::accept(b)) {
beammp_error("JsonDiff second argument is not valid json: `" + b + "`");
beammp_lua_error("JsonDiff second argument is not valid json: `" + b + "`");
return "";
}
auto a_json = nlohmann::json::parse(a);
@@ -567,13 +779,13 @@ std::string LuaAPI::Util::JsonDiff(const std::string& a, const std::string& b) {
return nlohmann::json::diff(a_json, b_json).dump();
}
std::string LuaAPI::Util::JsonDiffApply(const std::string& data, const std::string& patch) {
std::string LuaAPI::MP::JsonDiffApply(const std::string& data, const std::string& patch) {
if (!nlohmann::json::accept(data)) {
beammp_error("JsonDiffApply first argument is not valid json: `" + data + "`");
beammp_lua_error("JsonDiffApply first argument is not valid json: `" + data + "`");
return "";
}
if (!nlohmann::json::accept(patch)) {
beammp_error("JsonDiffApply second argument is not valid json: `" + patch + "`");
beammp_lua_error("JsonDiffApply second argument is not valid json: `" + patch + "`");
return "";
}
auto a_json = nlohmann::json::parse(data);
@@ -582,145 +794,38 @@ std::string LuaAPI::Util::JsonDiffApply(const std::string& data, const std::stri
return a_json.dump();
}
std::string LuaAPI::Util::JsonPrettify(const std::string& json) {
std::string LuaAPI::MP::JsonPrettify(const std::string& json) {
if (!nlohmann::json::accept(json)) {
beammp_error("JsonPrettify argument is not valid json: `" + json + "`");
beammp_lua_error("JsonPrettify argument is not valid json: `" + json + "`");
return "";
}
return nlohmann::json::parse(json).dump(4);
}
std::string LuaAPI::Util::JsonMinify(const std::string& json) {
std::string LuaAPI::MP::JsonMinify(const std::string& json) {
if (!nlohmann::json::accept(json)) {
beammp_error("JsonMinify argument is not valid json: `" + json + "`");
beammp_lua_error("JsonMinify argument is not valid json: `" + json + "`");
return "";
}
return nlohmann::json::parse(json).dump(-1);
}
std::string LuaAPI::Util::JsonFlatten(const std::string& json) {
std::string LuaAPI::MP::JsonFlatten(const std::string& json) {
if (!nlohmann::json::accept(json)) {
beammp_error("JsonFlatten argument is not valid json: `" + json + "`");
beammp_lua_error("JsonFlatten argument is not valid json: `" + json + "`");
return "";
}
return nlohmann::json::parse(json).flatten().dump(-1);
}
std::string LuaAPI::Util::JsonUnflatten(const std::string& json) {
std::string LuaAPI::MP::JsonUnflatten(const std::string& json) {
if (!nlohmann::json::accept(json)) {
beammp_error("JsonUnflatten argument is not valid json: `" + json + "`");
beammp_lua_error("JsonUnflatten argument is not valid json: `" + json + "`");
return "";
}
return nlohmann::json::parse(json).unflatten().dump(-1);
}
std::pair<bool, std::string> LuaAPI::MP::TriggerClientEventJson(int PlayerID, const std::string& EventName, const sol::table& Data) {
return InternalTriggerClientEvent(PlayerID, EventName, Util::JsonEncode(Data));
}
size_t LuaAPI::MP::GetPlayerCount() { return 0; }
template <typename T>
static void AddToTable(sol::table& table, const std::string& left, const T& value) {
if (left.empty()) {
table[table.size() + 1] = value;
} else {
table[left] = value;
}
}
static void JsonDecodeRecursive(sol::state_view& StateView, sol::table& table, const std::string& left, const nlohmann::json& right) {
switch (right.type()) {
case nlohmann::detail::value_t::null:
return;
case nlohmann::detail::value_t::object: {
auto value = table.create();
value.clear();
for (const auto& entry : right.items()) {
JsonDecodeRecursive(StateView, value, entry.key(), entry.value());
}
AddToTable(table, left, value);
break;
}
case nlohmann::detail::value_t::array: {
auto value = table.create();
value.clear();
for (const auto& entry : right.items()) {
JsonDecodeRecursive(StateView, value, "", entry.value());
}
AddToTable(table, left, value);
break;
}
case nlohmann::detail::value_t::string:
AddToTable(table, left, right.get<std::string>());
break;
case nlohmann::detail::value_t::boolean:
AddToTable(table, left, right.get<bool>());
break;
case nlohmann::detail::value_t::number_integer:
AddToTable(table, left, right.get<int64_t>());
break;
case nlohmann::detail::value_t::number_unsigned:
AddToTable(table, left, right.get<uint64_t>());
break;
case nlohmann::detail::value_t::number_float:
AddToTable(table, left, right.get<double>());
break;
case nlohmann::detail::value_t::binary:
beammp_errorf("JsonDecode can't handle binary blob in json, ignoring");
return;
case nlohmann::detail::value_t::discarded:
return;
default:
beammp_assert_not_reachable();
}
}
sol::table LuaAPI::Util::JsonDecode(sol::this_state s, const std::string& str) {
sol::state_view StateView(s.lua_state());
auto table = StateView.create_table(StateView);
if (!nlohmann::json::accept(str)) {
beammp_error("string given to JsonDecode is not valid json: `" + str + "`");
return sol::lua_nil;
}
nlohmann::json json = nlohmann::json::parse(str);
if (json.is_object()) {
for (const auto& entry : json.items()) {
JsonDecodeRecursive(StateView, table, entry.key(), entry.value());
}
} else if (json.is_array()) {
for (const auto& entry : json) {
JsonDecodeRecursive(StateView, table, "", entry);
}
} else {
beammp_error("JsonDecode expected array or object json, instead got " + std::string(json.type_name()));
return sol::lua_nil;
}
return table;
}
sol::table LuaAPI::FS::ListDirectories(sol::this_state s, const std::string& Path) {
if (!std::filesystem::exists(Path)) {
return sol::lua_nil;
}
auto table = sol::state_view(s.lua_state()).create_table();
for (const auto& entry : std::filesystem::directory_iterator(Path)) {
if (entry.is_directory()) {
table[table.size() + 1] = entry.path().lexically_relative(Path).string();
}
}
return table;
}
sol::table LuaAPI::FS::ListFiles(sol::this_state s, const std::string& Path) {
if (!std::filesystem::exists(Path)) {
return sol::lua_nil;
}
auto table = sol::state_view(s.lua_state()).create_table();
for (const auto& entry : std::filesystem::directory_iterator(Path)) {
if (entry.is_regular_file() || entry.is_symlink()) {
table[table.size() + 1] = entry.path().lexically_relative(Path).string();
}
}
return table;
return InternalTriggerClientEvent(PlayerID, EventName, JsonEncode(Data));
}
-703
View File
@@ -1,703 +0,0 @@
#include "LuaPlugin.h"
#include "Common.h"
#include "LuaAPI.h"
#include "Value.h"
#include <boost/algorithm/string/replace.hpp>
#include <boost/asio/post.hpp>
#include <boost/date_time/microsec_time_clock.hpp>
#include <boost/date_time/posix_time/posix_time_config.hpp>
#include <boost/date_time/posix_time/posix_time_duration.hpp>
#include <boost/thread/exceptions.hpp>
#include <cctype>
#include <chrono>
#include <filesystem>
#include <functional>
#include <lauxlib.h>
#include <lua.h>
#include <optional>
#include <regex>
#include <sol/forward.hpp>
#include <sol/sol.hpp>
#include <sol/types.hpp>
#include <sol/variadic_args.hpp>
#include <spdlog/common.h>
#include <spdlog/spdlog.h>
#include <sstream>
#include <thread>
static int lua_panic_handler(lua_State* state) {
sol::state_view view(state);
sol::state new_state {};
luaL_traceback(state, new_state.lua_state(), nullptr, 1);
auto traceback = new_state.get<std::string>(-1);
beammp_errorf("Lua panic (unclear in which plugin): {}", traceback);
return 1;
}
#define beammp_lua_debugf(...) beammp_debugf("[{}] {}", name(), fmt::format(__VA_ARGS__))
#define beammp_lua_infof(...) beammp_infof("[{}] {}", name(), fmt::format(__VA_ARGS__))
#define beammp_lua_warnf(...) beammp_warnf("[{}] {}", name(), fmt::format(__VA_ARGS__))
#define beammp_lua_errorf(...) beammp_errorf("[{}] {}", name(), fmt::format(__VA_ARGS__))
#define beammp_lua_tracef(...) beammp_tracef("[{}] {}", name(), fmt::format(__VA_ARGS__))
static constexpr const char* ERR_HANDLER = "__beammp_lua_error_handler";
/// Checks whether the supplied name is a valid lua identifier (mostly).
static inline bool check_name_validity(const std::string& name) {
if (name.empty()) {
return false;
}
if (std::isdigit(name.at(0))) {
return false;
}
for (const char c : name) {
if (!std::isalnum(c) && c != '_') {
return false;
}
}
return true;
}
LuaPlugin::LuaPlugin(const std::string& path)
: m_path(path) {
m_state = sol::state(lua_panic_handler);
m_thread = boost::scoped_thread<> { &LuaPlugin::thread_main, this };
}
LuaPlugin::~LuaPlugin() {
for (auto& timer : m_timers) {
timer->timer.cancel();
}
// work guard reset means that we allow all work to be finished before exit
m_work_guard.reset();
// setting this flag signals the thread to shut down
*m_shutdown = true;
}
Error LuaPlugin::initialize_error_handlers() {
m_state.set_exception_handler([](lua_State* state, sol::optional<const std::exception&>, auto err) -> int {
beammp_errorf("Error (unclear in which plugin): {}", err); // TODO: wtf?
return sol::stack::push(state, err);
});
m_state.globals()[ERR_HANDLER] = [this](const std::string& error) {
beammp_lua_errorf("Error: {}", error);
return error;
};
return {};
}
Error LuaPlugin::initialize_libraries() {
m_state.open_libraries(
sol::lib::base,
sol::lib::package,
sol::lib::coroutine,
sol::lib::string,
sol::lib::os,
sol::lib::math,
sol::lib::table,
sol::lib::debug,
sol::lib::bit32,
sol::lib::io);
auto& glob = m_state.globals();
glob.create_named("MP");
glob["MP"]["GetExtensions"] = [this]() -> sol::table {
auto table = m_state.create_table();
auto extensions = m_known_extensions.synchronize();
for (const auto& [ext, path] : *extensions) {
(void)path;
table[ext] = m_state.globals()[ext];
}
return table; };
glob["MP"]["GetStateMemoryUsage"] = [this]() { return size_t(m_state.memory_used()); };
glob["MP"]["GetPluginMemoryUsage"] = [this] { return memory_usage(); };
glob["MP"]["LogError"] = [this](const sol::variadic_args& args) {
auto result = print_impl({ args.begin(), args.end() });
beammp_lua_errorf("[out] {}", result);
};
glob["MP"]["LogWarn"] = [this](const sol::variadic_args& args) {
auto result = print_impl({ args.begin(), args.end() });
beammp_lua_warnf("[out] {}", result);
};
glob["MP"]["LogInfo"] = [this](const sol::variadic_args& args) {
auto result = print_impl({ args.begin(), args.end() });
beammp_lua_infof("[out] {}", result);
};
glob["MP"]["LogDebug"] = [this](const sol::variadic_args& args) {
auto result = print_impl({ args.begin(), args.end() });
beammp_lua_debugf("[out] {}", result);
};
glob["MP"]["GetPluginPath"] = [this] {
return std::filesystem::absolute(m_path).string();
};
glob["MP"]["RegisterEvent"] = [this](const std::string& event_name, const sol::object& handler) {
if (handler.get_type() == sol::type::string) {
m_event_handlers_named[event_name].push_back(handler.as<std::string>());
} else if (handler.get_type() == sol::type::function) {
auto fn = handler.as<sol::protected_function>();
fn.set_error_handler(m_state.globals()[ERR_HANDLER]);
m_event_handlers[event_name].push_back(fn);
} else {
beammp_lua_errorf("Invalid call to MP.RegisterEvent for event '{}': Expected string or function as second argument", event_name);
}
};
glob["MP"]["Post"] = [this](const sol::protected_function& fn) {
boost::asio::post(m_io, [fn] {
fn();
});
};
glob["MP"]["TriggerLocalEvent"] = [this](const std::string& event_name, sol::variadic_args args) -> sol::table {
std::vector<sol::object> args_vec(args.begin(), args.end());
ValueTuple values {};
values.reserve(args_vec.size());
for (const auto& obj : args_vec) {
auto res = sol_obj_to_value(obj);
if (res) [[likely]] {
values.emplace_back(res.move());
} else {
beammp_lua_errorf("Can't serialize an argument across boundaries (for passing to a MP.TriggerLocalEvent): {}", res.error);
values.emplace_back(Null {});
}
}
auto results = handle_event(event_name, std::make_shared<Value>(std::move(values)));
auto result = m_state.create_table();
result["__INTERNAL"] = results;
// waits for a specific number of milliseconds for a result to be available, returns false if timed out, true if ready.
result["WaitForMS"] = [](const sol::object& self, int milliseconds) {
std::shared_future<std::vector<Value>> future = self.as<sol::table>()["__INTERNAL"];
auto status = future.wait_for(std::chrono::milliseconds(milliseconds));
return status == std::future_status::ready;
};
// waits indefinitely for all results to be available
result["Wait"] = [](const sol::table& self) {
std::shared_future<std::vector<Value>> future = self["__INTERNAL"];
future.wait();
};
// waits indefinitely for all results to be available, then returns them
result["Results"] = [this](const sol::table& self) -> sol::table {
std::shared_future<std::vector<Value>> future = self["__INTERNAL"];
future.wait();
Value results = future.get();
auto lua_res = boost::apply_visitor(ValueToLuaVisitor(m_state), results);
return lua_res;
};
return result;
};
glob["MP"]["ScheduleCallRepeat"] = [this](size_t ms, sol::function fn, sol::variadic_args args) {
return l_mp_schedule_call_repeat(ms, fn, args);
};
glob["MP"]["ScheduleCallOnce"] = [this](size_t ms, sol::function fn, sol::variadic_args args) {
l_mp_schedule_call_once(ms, fn, args);
};
glob["MP"]["CancelScheduledCall"] = [this](std::shared_ptr<Timer>& timer) {
// this has to be post()-ed, otherwise the call will not cancel (not sure why)
boost::asio::post(m_io, [this, timer] {
if (!timer) {
beammp_lua_errorf("MP.cancel_scheduled_call: timer already cancelled");
return;
}
beammp_lua_debugf("Cancelling timer");
cancel_timer(timer);
});
// release the lua's reference to this timer
timer.reset();
};
glob["MP"]["GetOSName"] = &LuaAPI::MP::GetOSName;
// glob["MP"]["GetTimeMS"] = &LuaAPI::MP::GetTimeMS;
// glob["MP"]["GetTimeS"] = &LuaAPI::MP::GetTimeS;
glob.create_named("Util");
glob["Util"]["JsonEncode"] = &LuaAPI::Util::JsonEncode;
glob["Util"]["JsonDiff"] = &LuaAPI::Util::JsonDiff;
glob["Util"]["JsonDiffApply"] = &LuaAPI::Util::JsonDiffApply;
glob["Util"]["JsonPrettify"] = &LuaAPI::Util::JsonPrettify;
glob["Util"]["JsonMinify"] = &LuaAPI::Util::JsonMinify;
glob["Util"]["JsonFlatten"] = &LuaAPI::Util::JsonFlatten;
glob["Util"]["JsonUnflatten"] = &LuaAPI::Util::JsonUnflatten;
glob["Util"]["JsonDecode"] = &LuaAPI::Util::JsonDecode;
glob.create_named("FS");
glob["FS"]["Exists"] = &LuaAPI::FS::Exists;
glob["FS"]["CreateDirectory"] = &LuaAPI::FS::CreateDirectory;
glob["FS"]["ConcatPaths"] = &LuaAPI::FS::ConcatPaths;
glob["FS"]["IsFile"] = &LuaAPI::FS::IsFile;
glob["FS"]["Remove"] = &LuaAPI::FS::Remove;
glob["FS"]["GetFilename"] = &LuaAPI::FS::GetFilename;
glob["FS"]["IsDirectory"] = &LuaAPI::FS::IsDirectory;
glob["FS"]["GetExtensinon"] = &LuaAPI::FS::GetExtension;
glob["FS"]["GetParentFolder"] = &LuaAPI::FS::GetParentFolder;
glob["FS"]["Copy"] = &LuaAPI::FS::Copy;
glob["FS"]["Rename"] = &LuaAPI::FS::Rename;
glob["FS"]["ListFiles"] = &LuaAPI::FS::ListFiles;
glob["FS"]["PathSep"] = fmt::format("{}", char(std::filesystem::path::preferred_separator));
return {};
}
Error LuaPlugin::load_files() {
// 1. look for main.lua, run that
// 2. look for extensions in extensions/, load those.
// make those globals based on the filename
// 3. call onInit by name (global)
auto extensions_folder = m_path / "extensions";
if (std::filesystem::exists(extensions_folder)
&& (std::filesystem::is_directory(extensions_folder)
|| std::filesystem::is_symlink(extensions_folder))) {
// TODO: Check that it points to a directory if its a symlink
beammp_lua_debugf("Found extensions/: {}", extensions_folder);
// load extensions from the folder, can't fail
auto n = load_extensions(extensions_folder);
beammp_lua_debugf("Loaded {} extensions.", n);
beammp_lua_debugf("Set up file watcher to watch extensions folder for changes");
// set up file watcher. this will watch for new extensions or for extensions which have
// changed (via modification time).
m_extensions_watcher.watch_files_in(extensions_folder);
// set up callback for when an extension changes.
// we simply reload the extension as if nothing happened :)
m_extensions_watch_conn = m_extensions_watcher.sig_file_changed.connect(
[this, extensions_folder](const std::filesystem::path& path) {
if (path.extension() != ".lua") {
return; // ignore
}
auto rel = std::filesystem::relative(path, extensions_folder).string();
rel = boost::algorithm::replace_all_copy(rel, "/", "_");
rel = boost::algorithm::replace_all_copy(rel, ".lua", "");
if (!check_name_validity(rel)) {
beammp_lua_errorf("Can't load/reload extension at path: {}. The resulting extension name would be invalid.", path);
} else {
load_extension(path, rel);
}
});
} else {
beammp_lua_debugf("Plugin '{}' has no extensions.", name());
}
if (m_path != BEAMMP_MEMORY_STATE) {
auto main_lua = m_path / "main.lua";
if (std::filesystem::exists(main_lua)) {
// TODO: Check that it's a regular file or symlink
beammp_lua_debugf("Found main.lua: {}", main_lua.string());
boost::asio::post(m_io, [this, main_lua] {
try {
m_state.safe_script_file(main_lua.string());
} catch (const std::exception& e) {
beammp_lua_errorf("Error running '{}': {}", main_lua.string(), e.what());
}
});
} else {
beammp_lua_warnf("No 'main.lua' found, a plugin should have a 'main.lua'.");
}
}
return {};
}
Error LuaPlugin::initialize_overrides() {
boost::asio::post(m_io, [this] {
m_state.globals()["print"] = [this](sol::variadic_args args) {
l_print(args);
};
});
Error err = fix_lua_paths();
if (err) {
return err;
}
return {};
}
Error LuaPlugin::fix_lua_paths() {
std::stringstream lua_paths;
std::stringstream lua_c_paths;
std::vector<std::filesystem::path> relevant_paths = {
m_path,
m_path / "extensions",
};
for (const auto& Path : relevant_paths) {
lua_paths << ";" << (Path / "?.lua").string();
lua_paths << ";" << (Path / "lua/?.lua").string();
#if WIN32
lua_c_paths << ";" << (Path / "?.dll").string();
lua_c_paths << ";" << (Path / "lib/?.dll").string();
#else // unix
lua_c_paths << ";" << (Path / "?.so").string();
lua_c_paths << ";" << (Path / "lib/?.so").string();
#endif
}
auto package_table = m_state.globals().get<sol::table>("package");
package_table["path"] = package_table.get<std::string>("path") + lua_paths.str();
package_table["cpath"] = package_table.get<std::string>("cpath") + lua_c_paths.str();
m_state.globals()["package"] = package_table;
return {};
}
void LuaPlugin::load_extension(const std::filesystem::path& file, const std::string& ext_name) {
// we have to assume that load_extension may be called at any time, even to reload an existing extension.
// thus, it cannot make assumptions about the plugin's status or state.
beammp_lua_debugf("Loading extension '{}' from {}", ext_name, file);
// save the extension in a list to make it queryable
m_known_extensions->insert_or_assign(ext_name, file);
// extension names, generated by the caller, must be valid lua identifiers
if (!check_name_validity(ext_name)) {
beammp_lua_errorf("Extension name '{}' is invalid. Please make sure the extension and it's folder(s) do not contain special characters, spaces, start with a digit, or similar.", ext_name);
return;
}
try {
auto result = m_state.safe_script_file(file.string());
if (!result.valid()) {
beammp_lua_errorf("Error loading extension '{}' from {}. Running file resulted in invalid state: {}. Please check for errors in the lines before this message", ext_name, file, sol::to_string(result.status()));
return;
} else if (result.get_type() != sol::type::table) {
beammp_lua_errorf("Error loading extension '{}' from {}: Expected extension to return a table, got {} instead.", ext_name, file, sol::type_name(m_state.lua_state(), result.get_type()));
return;
}
auto M = result.get<sol::table>();
m_state.globals()[ext_name] = M;
} catch (const std::exception& e) {
beammp_lua_errorf("Error loading extension '{}' from {}: {}", ext_name, file, e.what());
return;
}
beammp_lua_debugf("Extension '{}' loaded!", ext_name);
}
size_t LuaPlugin::load_extensions(const std::filesystem::path& extensions_folder, const std::string& base) {
std::filesystem::directory_iterator iter(extensions_folder, s_directory_iteration_options);
std::vector<std::filesystem::directory_entry> files;
std::vector<std::filesystem::directory_entry> directories;
for (const auto& entry : iter) {
if (entry.is_directory()) {
directories.push_back(entry);
} else if (entry.is_regular_file()) {
files.push_back(entry);
} else {
beammp_lua_tracef("{} is neither a file nor a directory, skipping", entry.path());
}
}
// sort files alphabetically
std::sort(files.begin(), files.end(), [&](const auto& a, const auto& b) {
auto as = a.path().filename().string();
auto bs = b.path().filename().string();
return std::lexicographical_compare(as.begin(), as.end(), bs.begin(), bs.end());
});
for (const auto& file : files) {
boost::asio::post(m_io, [this, base, file] {
std::string ext_name;
if (base.empty()) {
ext_name = file.path().stem().string();
} else {
ext_name = fmt::format("{}_{}", base, file.path().stem().string());
}
load_extension(file, ext_name);
});
}
std::sort(directories.begin(), directories.end(), [&](const auto& a, const auto& b) {
auto as = a.path().filename().string();
auto bs = b.path().filename().string();
return std::lexicographical_compare(as.begin(), as.end(), bs.begin(), bs.end());
});
size_t count = 0;
for (const auto& dir : directories) {
std::string ext_name = dir.path().filename().string();
std::filesystem::path path = dir.path();
count += load_extensions(path, ext_name);
}
return count + files.size();
}
void LuaPlugin::thread_main() {
RegisterThread(name());
beammp_lua_debugf("Waiting for initialization");
// wait for interruption
// we sleep for some time, which can be interrupted by a thread.interrupt(),
// which will cause the sleep to throw a boost::thread_interrupted exception.
// we (ab)use this to synchronize.
try {
boost::this_thread::sleep_for(boost::chrono::seconds(1));
} catch (boost::thread_interrupted) {
}
beammp_lua_debugf("Initialized!");
while (!*m_shutdown) {
auto ran = m_io.run_for(std::chrono::seconds(5));
// update the memory used by the Lua Plugin, then immediately resume execution of handlers
if (ran != 0) {
*m_memory = m_state.memory_used();
}
}
}
Error LuaPlugin::initialize() {
Error err = initialize_error_handlers();
if (err) {
return { "Failed to initialize error handlers: {}", err };
}
err = initialize_libraries();
if (err) {
return { "Failed to initialize libraries: {}", err };
}
err = initialize_overrides();
if (err) {
return { "Failed to initialize overrides: {}", err };
}
err = load_files();
if (err) {
return { "Failed to load initial files: {}", err };
}
// interrupt the thread, signalling it to start
m_thread.interrupt();
return {};
}
Error LuaPlugin::cleanup() {
// TODO
return {};
}
Error LuaPlugin::reload() {
// TODO
return {};
}
std::string LuaPlugin::name() const {
return m_path.stem().string();
}
std::filesystem::path LuaPlugin::path() const {
return m_path;
}
std::shared_future<std::vector<Value>> LuaPlugin::handle_event(const std::string& event_name, const std::shared_ptr<Value>& args) {
std::shared_ptr<std::promise<std::vector<Value>>> promise = std::make_shared<std::promise<std::vector<Value>>>();
std::shared_future<std::vector<Value>> futures { promise->get_future() };
boost::asio::post(m_io, [this, event_name, args, promise, futures] {
std::vector<Value> results {};
if (m_event_handlers_named.contains(event_name)) {
auto handler_names = m_event_handlers_named.at(event_name);
for (const auto& handler_name : handler_names) {
try {
if (m_state.globals()[handler_name].valid() && m_state.globals()[handler_name].get_type() == sol::type::function) {
auto fn = m_state.globals().get<sol::protected_function>(handler_name);
fn.set_error_handler(m_state.globals()[ERR_HANDLER]);
auto lua_args = boost::apply_visitor(ValueToLuaVisitor(m_state), *args);
sol::protected_function_result res;
if (args->which() == VALUE_TYPE_IDX_TUPLE) {
res = fn(sol::as_args(lua_args.as<std::vector<sol::object>>()));
} else {
res = fn(lua_args);
}
if (res.valid()) {
auto maybe_res = sol_obj_to_value(res.get<sol::object>());
if (maybe_res) {
results.emplace_back(maybe_res.move());
} else {
beammp_lua_errorf("Error using return value from event handler '{}' for event '{}': {}", handler_name, event_name, maybe_res.error);
}
}
} else {
beammp_lua_errorf("Invalid event handler '{}' for event '{}': Handler either doesn't exist or isn't a global function", handler_name, event_name);
}
} catch (const std::exception& e) {
beammp_lua_errorf("Error finding and running event handler for event '{}': {}. It was called with argument(s): {}", event_name, e.what(), boost::apply_visitor(ValueToStringVisitor(), *args));
}
}
}
if (m_event_handlers.contains(event_name)) {
auto handlers = m_event_handlers.at(event_name);
for (const auto& fn : handlers) {
try {
auto lua_args = boost::apply_visitor(ValueToLuaVisitor(m_state), *args);
sol::protected_function_result res;
if (args->which() == VALUE_TYPE_IDX_TUPLE) {
res = fn(sol::as_args(lua_args.as<std::vector<sol::object>>()));
} else {
res = fn(lua_args);
}
if (res.valid()) {
auto maybe_res = sol_obj_to_value(res.get<sol::object>());
if (maybe_res) {
results.emplace_back(maybe_res.move());
} else {
beammp_lua_errorf("Error using return value from event handler '<<lua function {:p}>>' for event '{}': {}", fn.pointer(), event_name, maybe_res.error);
}
}
} catch (const std::exception& e) {
beammp_lua_errorf("Error finding and running event handler for event '{}': {}. It was called with argument(s): {}", event_name, e.what(), boost::apply_visitor(ValueToStringVisitor(), *args));
}
}
}
promise->set_value(std::move(results));
});
return futures;
}
size_t LuaPlugin::memory_usage() const {
return *m_memory;
}
std::shared_ptr<Timer> LuaPlugin::make_timer(size_t ms) {
m_timers.push_back(std::make_shared<Timer>(boost::asio::deadline_timer(m_io), ms));
auto timer = m_timers.back();
timer->timer.expires_from_now(timer->interval);
std::sort(m_timers.begin(), m_timers.end());
return timer;
}
void LuaPlugin::cancel_timer(const std::shared_ptr<Timer>& timer) {
auto iter = std::find(m_timers.begin(), m_timers.end(), timer);
if (iter != m_timers.end()) {
m_timers.erase(iter);
timer->timer.cancel();
} else {
timer->timer.cancel();
beammp_lua_debugf("Failed to remove timer (already removed)");
}
}
void LuaPlugin::l_print(const sol::variadic_args& args) {
auto result = print_impl({ args.begin(), args.end() });
beammp_lua_infof("{}", result);
}
void LuaPlugin::l_mp_schedule_call_helper(const boost::system::error_code& err, std::shared_ptr<Timer> timer, const sol::function& fn, std::shared_ptr<ValueTuple> args) {
if (err) {
beammp_lua_debugf("MP.schedule_call_repeat: {}", err.what());
return;
}
timer->timer.expires_from_now(timer->interval);
sol::protected_function prot(fn);
prot.set_error_handler(m_state.globals()[ERR_HANDLER]);
std::vector<sol::object> objs;
objs.reserve(args->size());
for (const auto& val : *args) {
objs.push_back(boost::apply_visitor(ValueToLuaVisitor(m_state), val));
}
prot(sol::as_args(objs));
timer->timer.async_wait([this, timer, fn, args](const auto& err) {
l_mp_schedule_call_helper(err, timer, fn, args);
});
}
void LuaPlugin::l_mp_schedule_call_once(size_t ms, const sol::function& fn, sol::variadic_args args) {
auto timer = make_timer(ms);
std::vector<sol::object> args_vec(args.begin(), args.end());
std::shared_ptr<ValueTuple> tuple = std::make_shared<ValueTuple>();
tuple->reserve(args_vec.size());
for (const auto& obj : args_vec) {
auto res = sol_obj_to_value(obj);
if (res) [[likely]] {
tuple->emplace_back(res.move());
} else {
beammp_lua_errorf("Can't serialize an argument across boundaries (for passing to a MP.schedule_call_* later): ", res.error);
tuple->emplace_back(Null {});
}
}
timer->timer.async_wait([this, timer, fn, tuple](const auto& err) {
if (err) {
beammp_lua_debugf("MP.schedule_call_once: {}", err.what());
return;
}
sol::protected_function prot(fn);
prot.set_error_handler(m_state.globals()[ERR_HANDLER]);
std::vector<sol::object> objs;
objs.reserve(tuple->size());
for (const auto& val : *tuple) {
objs.push_back(boost::apply_visitor(ValueToLuaVisitor(m_state), val));
}
beammp_lua_debugf("Calling with {} args", objs.size());
prot(sol::as_args(objs));
cancel_timer(timer);
});
}
std::shared_ptr<Timer> LuaPlugin::l_mp_schedule_call_repeat(size_t ms, const sol::function& fn, sol::variadic_args args) {
auto timer = make_timer(ms);
// TODO: Cleanly transfer invalid objects
std::vector<sol::object> args_vec(args.begin(), args.end());
std::shared_ptr<ValueTuple> tuple = std::make_shared<ValueTuple>();
tuple->reserve(args_vec.size());
for (const auto& obj : args_vec) {
auto res = sol_obj_to_value(obj);
if (res) [[likely]] {
tuple->emplace_back(res.move());
} else {
beammp_lua_errorf("Can't serialize an argument across boundaries (for passing to a MP.schedule_call_* later): ", res.error);
tuple->emplace_back(Null {});
}
}
timer->timer.async_wait([this, timer, fn, tuple](const auto& err) {
l_mp_schedule_call_helper(err, timer, fn, tuple);
});
return timer;
}
std::string LuaPlugin::print_impl(const std::vector<sol::object>& obj_args) {
std::string result {};
result.reserve(500);
// used as the invalid value provider in sol_obj_to_value.
auto special_stringifier = [](const sol::object& object) -> Result<Value> {
switch (object.get_type()) {
case sol::type::none:
case sol::type::lua_nil:
case sol::type::string:
case sol::type::number:
case sol::type::boolean:
case sol::type::table:
// covered by value to string visitor
break;
case sol::type::thread:
return { fmt::format("<<lua thread: {:p}>>", object.pointer()) };
case sol::type::function:
return { fmt::format("<<lua function: {:p}>>", object.pointer()) };
case sol::type::userdata:
return { fmt::format("<<lua userdata: {:p}>>", object.pointer()) };
case sol::type::lightuserdata:
return { fmt::format("<<lua lightuserdata: {:p}>>", object.pointer()) };
case sol::type::poly:
return { fmt::format("<<lua poly: {:p}>>", object.pointer()) };
default:
break;
}
return { fmt::format("<<lua unknown type: {:p}>>", object.pointer()) };
};
for (const auto& obj : obj_args) {
auto maybe_val = sol_obj_to_value(obj, special_stringifier);
if (maybe_val) {
result += boost::apply_visitor(ValueToStringVisitor(ValueToStringVisitor::Flag::NONE), maybe_val.move());
result += " ";
} else {
beammp_lua_errorf("Failed to print() an argument: {}", maybe_val.error);
}
}
return result;
}
void LuaPlugin::run_raw_lua(const std::string& raw) {
if (raw.empty()) {
return;
}
std::string id = "";
if (raw.size() < 5) {
id = raw;
} else {
id = raw.substr(0, 5) + "...";
}
boost::asio::post(m_io, [this, raw, id] {
try {
beammp_debugf("Running '{}'", raw);
auto res = m_state.safe_script(raw, id);
if (res.valid()) {
std::vector<sol::object> args;
args.push_back(res.get<sol::object>());
auto str = print_impl(args);
Application::Console().WriteRaw(fmt::format("=> {}", str));
}
} catch (const sol::error& err) {
beammp_lua_errorf("Error: {}", err.what());
}
});
}
-1254
View File
File diff suppressed because it is too large Load Diff
+59
View File
@@ -0,0 +1,59 @@
#include "Profiling.h"
#include <limits>
prof::Duration prof::duration(const TimePoint& start, const TimePoint& end) {
return end - start;
}
prof::TimePoint prof::now() {
return std::chrono::high_resolution_clock::now();
}
prof::Stats prof::UnitProfileCollection::stats(const std::string& unit) {
return m_map->operator[](unit).stats();
}
size_t prof::UnitProfileCollection::measurement_count(const std::string& unit) {
return m_map->operator[](unit).measurement_count();
}
void prof::UnitProfileCollection::add_sample(const std::string& unit, const Duration& duration) {
m_map->operator[](unit).add_sample(duration);
}
prof::Stats prof::UnitExecutionTime::stats() const {
std::unique_lock lock(m_mtx);
Stats result {};
// calculate sum
result.n = m_total_calls;
result.max = m_min;
result.min = m_max;
// calculate mean: mean = sum_x / n
result.mean = m_sum / double(m_total_calls);
// calculate stdev: stdev = sqrt((sum_x2 / n) - (mean * mean))
result.stdev = std::sqrt((m_measurement_sqr_sum / double(result.n)) - (result.mean * result.mean));
return result;
}
void prof::UnitExecutionTime::add_sample(const Duration& dur) {
std::unique_lock lock(m_mtx);
m_sum += dur.count();
m_measurement_sqr_sum += dur.count() * dur.count();
m_min = std::min(dur.count(), m_min);
m_max = std::max(dur.count(), m_max);
++m_total_calls;
}
prof::UnitExecutionTime::UnitExecutionTime() {
}
std::unordered_map<std::string, prof::Stats> prof::UnitProfileCollection::all_stats() {
auto map = m_map.synchronize();
std::unordered_map<std::string, Stats> result {};
for (const auto& [name, time] : *map) {
result[name] = time.stats();
}
return result;
}
size_t prof::UnitExecutionTime::measurement_count() const {
std::unique_lock lock(m_mtx);
return m_total_calls;
}
+191
View File
@@ -0,0 +1,191 @@
// BeamMP, the BeamNG.drive multiplayer mod.
// Copyright (C) 2024 BeamMP Ltd., BeamMP team and contributors.
//
// BeamMP Ltd. can be contacted by electronic mail via contact@beammp.com.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published
// by the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
#include "Settings.h"
Settings::Settings() {
SettingsMap = std::unordered_map<Key, SettingsTypeVariant> {
// All entries which contain std::strings must be explicitly constructed, otherwise they become 'bool'
{ General_Description, std::string("BeamMP Default Description") },
{ General_Tags, std::string("Freeroam") },
{ General_MaxPlayers, 8 },
{ General_Name, std::string("BeamMP Server") },
{ General_Map, std::string("/levels/gridmap_v2/info.json") },
{ General_AuthKey, std::string("") },
{ General_Private, true },
{ General_IP, "::"},
{ General_Port, 30814 },
{ General_MaxCars, 1 },
{ General_LogChat, true },
{ General_ResourceFolder, std::string("Resources") },
{ General_Debug, false },
{ General_AllowGuests, true },
{ General_InformationPacket, true },
{ Misc_ImScaredOfUpdates, true },
{ Misc_UpdateReminderTime, "30s" }
};
InputAccessMapping = std::unordered_map<ComposedKey, SettingsAccessControl> {
{ { "General", "Description" }, { General_Description, READ_WRITE } },
{ { "General", "Tags" }, { General_Tags, READ_WRITE } },
{ { "General", "MaxPlayers" }, { General_MaxPlayers, READ_WRITE } },
{ { "General", "Name" }, { General_Name, READ_WRITE } },
{ { "General", "Map" }, { General_Map, READ_WRITE } },
{ { "General", "AuthKey" }, { General_AuthKey, NO_ACCESS } },
{ { "General", "Private" }, { General_Private, READ_ONLY } },
{ { "General", "IP" }, { General_IP, READ_ONLY } },
{ { "General", "Port" }, { General_Port, READ_ONLY } },
{ { "General", "MaxCars" }, { General_MaxCars, READ_WRITE } },
{ { "General", "LogChat" }, { General_LogChat, READ_ONLY } },
{ { "General", "ResourceFolder" }, { General_ResourceFolder, READ_ONLY } },
{ { "General", "Debug" }, { General_Debug, READ_WRITE } },
{ { "General", "AllowGuests" }, { General_AllowGuests, READ_WRITE } },
{ { "General", "InformationPacket" }, { General_InformationPacket, READ_WRITE } },
{ { "Misc", "ImScaredOfUpdates" }, { Misc_ImScaredOfUpdates, READ_WRITE } },
{ { "Misc", "UpdateReminderTime" }, { Misc_UpdateReminderTime, READ_WRITE } }
};
}
std::string Settings::getAsString(Key key) {
auto map = SettingsMap.synchronize();
if (!map->contains(key)) {
throw std::logic_error { "Undefined key accessed in Settings::getAsString" };
}
return std::get<std::string>(map->at(key));
}
int Settings::getAsInt(Key key) {
auto map = SettingsMap.synchronize();
if (!map->contains(key)) {
throw std::logic_error { "Undefined key accessed in Settings::getAsInt" };
}
return std::get<int>(map->at(key));
}
bool Settings::getAsBool(Key key) {
auto map = SettingsMap.synchronize();
if (!map->contains(key)) {
throw std::logic_error { "Undefined key accessed in Settings::getAsBool" };
}
return std::get<bool>(map->at(key));
}
Settings::SettingsTypeVariant Settings::get(Key key) {
auto map = SettingsMap.synchronize();
if (!map->contains(key)) {
throw std::logic_error { "Undefined setting key accessed in Settings::get" };
}
return map->at(key);
}
void Settings::set(Key key, const std::string& value) {
auto map = SettingsMap.synchronize();
if (!map->contains(key)) {
throw std::logic_error { "Undefined setting key accessed in Settings::set(std::string)" };
}
if (!std::holds_alternative<std::string>(map->at(key))) {
throw std::logic_error { fmt::format("Wrong value type in Settings::set(std::string): index {}", map->at(key).index()) };
}
map->at(key) = value;
}
const std::unordered_map<ComposedKey, Settings::SettingsAccessControl> Settings::getAccessControlMap() const {
return *InputAccessMapping;
}
Settings::SettingsAccessControl Settings::getConsoleInputAccessMapping(const ComposedKey& keyName) {
auto acl_map = InputAccessMapping.synchronize();
if (!acl_map->contains(keyName)) {
throw std::logic_error { "Unknown key name accessed in Settings::getConsoleInputAccessMapping" };
} else if (acl_map->at(keyName).second == SettingsAccessMask::NO_ACCESS) {
throw std::logic_error { "Setting '" + keyName.Category + "::" + keyName.Key + "' is not accessible from within the runtime!" };
}
return acl_map->at(keyName);
}
void Settings::setConsoleInputAccessMapping(const ComposedKey& keyName, const std::string& value) {
auto [map, acl_map] = boost::synchronize(SettingsMap, InputAccessMapping);
if (!acl_map->contains(keyName)) {
throw std::logic_error { "Unknown key name accessed in Settings::setConsoleInputAccessMapping" };
} else if (acl_map->at(keyName).second == SettingsAccessMask::NO_ACCESS) {
throw std::logic_error { "Setting '" + keyName.Category + "::" + keyName.Key + "' is not accessible from within the runtime!" };
} else if (acl_map->at(keyName).second == SettingsAccessMask::READ_ONLY) {
throw std::logic_error { "Setting '" + keyName.Category + "::" + keyName.Key + "' is not writeable from within the runtime!" };
}
Key key = acl_map->at(keyName).first;
if (!std::holds_alternative<std::string>(map->at(key))) {
throw std::logic_error { "Wrong value type in Settings::setConsoleInputAccessMapping: expected std::string" };
}
map->at(key) = value;
}
void Settings::setConsoleInputAccessMapping(const ComposedKey& keyName, int value) {
auto [map, acl_map] = boost::synchronize(SettingsMap, InputAccessMapping);
if (!acl_map->contains(keyName)) {
throw std::logic_error { "Unknown key name accessed in Settings::setConsoleInputAccessMapping" };
} else if (acl_map->at(keyName).second == SettingsAccessMask::NO_ACCESS) {
throw std::logic_error { "Key '" + keyName.Category + "::" + keyName.Key + "' is not accessible from within the runtime!" };
} else if (acl_map->at(keyName).second == SettingsAccessMask::READ_ONLY) {
throw std::logic_error { "Key '" + keyName.Category + "::" + keyName.Key + "' is not writeable from within the runtime!" };
}
Key key = acl_map->at(keyName).first;
if (!std::holds_alternative<int>(map->at(key))) {
throw std::logic_error { "Wrong value type in Settings::setConsoleInputAccessMapping: expected int" };
}
map->at(key) = value;
}
void Settings::setConsoleInputAccessMapping(const ComposedKey& keyName, bool value) {
auto [map, acl_map] = boost::synchronize(SettingsMap, InputAccessMapping);
if (!acl_map->contains(keyName)) {
throw std::logic_error { "Unknown key name accessed in Settings::setConsoleInputAccessMapping" };
} else if (acl_map->at(keyName).second == SettingsAccessMask::NO_ACCESS) {
throw std::logic_error { "Key '" + keyName.Category + "::" + keyName.Key + "' is not accessible from within the runtime!" };
} else if (acl_map->at(keyName).second == SettingsAccessMask::READ_ONLY) {
throw std::logic_error { "Key '" + keyName.Category + "::" + keyName.Key + "' is not writeable from within the runtime!" };
}
Key key = acl_map->at(keyName).first;
if (!std::holds_alternative<bool>(map->at(key))) {
throw std::logic_error { "Wrong value type in Settings::setConsoleInputAccessMapping: expected bool" };
}
map->at(key) = value;
}
TEST_CASE("settings get/set") {
Settings settings;
settings.set(Settings::General_Name, "hello, world");
CHECK_EQ(settings.getAsString(Settings::General_Name), "hello, world");
settings.set(Settings::General_Name, std::string("hello, world"));
CHECK_EQ(settings.getAsString(Settings::General_Name), "hello, world");
settings.set(Settings::General_MaxPlayers, 12);
CHECK_EQ(settings.getAsInt(Settings::General_MaxPlayers), 12);
}
TEST_CASE("settings check for exception on wrong input type") {
Settings settings;
CHECK_THROWS(settings.set(Settings::General_Debug, "hello, world"));
CHECK_NOTHROW(settings.set(Settings::General_Debug, false));
}
+150 -147
View File
@@ -18,18 +18,24 @@
#include "Common.h"
#include "Env.h"
#include "Settings.h"
#include "TConfig.h"
#include <cstdlib>
#include <exception>
#include <fstream>
#include <iostream>
#include <istream>
#include <sstream>
#include <type_traits>
// General
static constexpr std::string_view StrDebug = "Debug";
static constexpr std::string_view EnvStrDebug = "BEAMMP_DEBUG";
static constexpr std::string_view StrPrivate = "Private";
static constexpr std::string_view EnvStrPrivate = "BEAMMP_PRIVATE";
static constexpr std::string_view StrIP = "IP";
static constexpr std::string_view EnvStrIP = "BEAMMP_IP";
static constexpr std::string_view StrPort = "Port";
static constexpr std::string_view EnvStrPort = "BEAMMP_PORT";
static constexpr std::string_view StrMaxCars = "MaxCars";
@@ -50,12 +56,17 @@ static constexpr std::string_view StrAuthKey = "AuthKey";
static constexpr std::string_view EnvStrAuthKey = "BEAMMP_AUTH_KEY";
static constexpr std::string_view StrLogChat = "LogChat";
static constexpr std::string_view EnvStrLogChat = "BEAMMP_LOG_CHAT";
static constexpr std::string_view StrAllowGuests = "AllowGuests";
static constexpr std::string_view EnvStrAllowGuests = "BEAMMP_ALLOW_GUESTS";
static constexpr std::string_view StrInformationPacket = "InformationPacket";
static constexpr std::string_view EnvStrInformationPacket = "BEAMMP_INFORMATION_PACKET";
static constexpr std::string_view StrPassword = "Password";
// Misc
static constexpr std::string_view StrSendErrors = "SendErrors";
static constexpr std::string_view StrSendErrorsMessageEnabled = "SendErrorsShowMessage";
static constexpr std::string_view StrHideUpdateMessages = "ImScaredOfUpdates";
static constexpr std::string_view EnvStrHideUpdateMessages = "BEAMMP_IM_SCARED_OF_UPDATES";
static constexpr std::string_view StrUpdateReminderTime = "UpdateReminderTime";
static constexpr std::string_view EnvStrUpdateReminderTime = "BEAMMP_UPDATE_REMINDER_TIME";
TEST_CASE("TConfig::TConfig") {
const std::string CfgFile = "beammp_server_testconfig.toml";
@@ -87,7 +98,9 @@ TEST_CASE("TConfig::TConfig") {
TConfig::TConfig(const std::string& ConfigFileName)
: mConfigFileName(ConfigFileName) {
Application::SetSubsystemStatus("Config", Application::Status::Starting);
if (!fs::exists(mConfigFileName) || !fs::is_regular_file(mConfigFileName)) {
auto DisableConfig = Env::Get(Env::Key::PROVIDER_DISABLE_CONFIG).value_or("false");
mDisableConfig = DisableConfig == "true" || DisableConfig == "1";
if (!mDisableConfig && (!fs::exists(mConfigFileName) || !fs::is_regular_file(mConfigFileName))) {
beammp_info("No config file found! Generating one...");
CreateConfigFile();
}
@@ -117,35 +130,39 @@ void SetComment(CommentsT& Comments, const std::string& Comment) {
void TConfig::FlushToFile() {
// auto data = toml::parse<toml::preserve_comments>(mConfigFileName);
auto data = toml::value {};
data["General"][StrAuthKey.data()] = Application::Settings.Key;
data["General"][StrAuthKey.data()] = Application::Settings.getAsString(Settings::Key::General_AuthKey);
SetComment(data["General"][StrAuthKey.data()].comments(), " AuthKey has to be filled out in order to run the server");
data["General"][StrLogChat.data()] = Application::Settings.LogChat;
data["General"][StrLogChat.data()] = Application::Settings.getAsBool(Settings::Key::General_LogChat);
SetComment(data["General"][StrLogChat.data()].comments(), " Whether to log chat messages in the console / log");
data["General"][StrDebug.data()] = Application::Settings.DebugModeEnabled;
data["General"][StrPrivate.data()] = Application::Settings.Private;
data["General"][StrPort.data()] = Application::Settings.Port;
data["General"][StrName.data()] = Application::Settings.ServerName;
data["General"][StrDebug.data()] = Application::Settings.getAsBool(Settings::Key::General_Debug);
data["General"][StrPrivate.data()] = Application::Settings.getAsBool(Settings::Key::General_Private);
SetComment(data["General"][StrInformationPacket.data()].comments(), " Whether to allow unconnected clients to get the public server information without joining");
data["General"][StrInformationPacket.data()] = Application::Settings.getAsBool(Settings::Key::General_InformationPacket);
data["General"][StrAllowGuests.data()] = Application::Settings.getAsBool(Settings::Key::General_AllowGuests);
SetComment(data["General"][StrAllowGuests.data()].comments(), " Whether to allow guests");
data["General"][StrIP.data()] = Application::Settings.getAsString(Settings::Key::General_IP);
SetComment(data["General"][StrIP.data()].comments(), " The IP address to bind the server to, this is NOT related to your public IP. Can be used if your machine has multiple network interfaces");
data["General"][StrPort.data()] = Application::Settings.getAsInt(Settings::Key::General_Port);
data["General"][StrName.data()] = Application::Settings.getAsString(Settings::Key::General_Name);
SetComment(data["General"][StrTags.data()].comments(), " Add custom identifying tags to your server to make it easier to find. Format should be TagA,TagB,TagC. Note the comma seperation.");
data["General"][StrTags.data()] = Application::Settings.ServerTags;
data["General"][StrMaxCars.data()] = Application::Settings.MaxCars;
data["General"][StrMaxPlayers.data()] = Application::Settings.MaxPlayers;
data["General"][StrMap.data()] = Application::Settings.MapName;
data["General"][StrDescription.data()] = Application::Settings.ServerDesc;
data["General"][StrResourceFolder.data()] = Application::Settings.Resource;
data["General"][StrTags.data()] = Application::Settings.getAsString(Settings::Key::General_Tags);
data["General"][StrMaxCars.data()] = Application::Settings.getAsInt(Settings::Key::General_MaxCars);
data["General"][StrMaxPlayers.data()] = Application::Settings.getAsInt(Settings::Key::General_MaxPlayers);
data["General"][StrMap.data()] = Application::Settings.getAsString(Settings::Key::General_Map);
data["General"][StrDescription.data()] = Application::Settings.getAsString(Settings::Key::General_Description);
data["General"][StrResourceFolder.data()] = Application::Settings.getAsString(Settings::Key::General_ResourceFolder);
// data["General"][StrPassword.data()] = Application::Settings.Password;
// SetComment(data["General"][StrPassword.data()].comments(), " Sets a password on this server, which restricts people from joining. To join, a player must enter this exact password. Leave empty ("") to disable the password.");
// Misc
data["Misc"][StrHideUpdateMessages.data()] = Application::Settings.HideUpdateMessages;
data["Misc"][StrHideUpdateMessages.data()] = Application::Settings.getAsBool(Settings::Key::Misc_ImScaredOfUpdates);
SetComment(data["Misc"][StrHideUpdateMessages.data()].comments(), " Hides the periodic update message which notifies you of a new server version. You should really keep this on and always update as soon as possible. For more information visit https://wiki.beammp.com/en/home/server-maintenance#updating-the-server. An update message will always appear at startup regardless.");
data["Misc"][StrSendErrors.data()] = Application::Settings.SendErrors;
SetComment(data["Misc"][StrSendErrors.data()].comments(), " If SendErrors is `true`, the server will send helpful info about crashes and other issues back to the BeamMP developers. This info may include your config, who is on your server at the time of the error, and similar general information. This kind of data is vital in helping us diagnose and fix issues faster. This has no impact on server performance. You can opt-out of this system by setting this to `false`");
data["Misc"][StrSendErrorsMessageEnabled.data()] = Application::Settings.SendErrorsMessageEnabled;
SetComment(data["Misc"][StrSendErrorsMessageEnabled.data()].comments(), " You can turn on/off the SendErrors message you get on startup here");
data["Misc"][StrUpdateReminderTime.data()] = Application::Settings.getAsString(Settings::Key::Misc_UpdateReminderTime);
SetComment(data["Misc"][StrUpdateReminderTime.data()].comments(), " Specifies the time between update reminders. You can use any of \"s, min, h, d\" at the end to specify the units seconds, minutes, hours or days. So 30d or 0.5min will print the update message every 30 days or half a minute.");
std::stringstream Ss;
Ss << "# This is the BeamMP-Server config file.\n"
"# Help & Documentation: `https://wiki.beammp.com/en/home/server-maintenance`\n"
"# Help & Documentation: `https://docs.beammp.com/server/server-maintenance/`\n"
"# IMPORTANT: Fill in the AuthKey with the key you got from `https://keymaster.beammp.com/` on the left under \"Keys\"\n"
<< data;
<< toml::format(data);
auto File = std::fopen(mConfigFileName.c_str(), "w+");
if (!File) {
beammp_error("Failed to create/write to config file: " + GetPlatformAgnosticErrorString());
@@ -161,172 +178,158 @@ void TConfig::FlushToFile() {
void TConfig::CreateConfigFile() {
// build from old config Server.cfg
try {
if (fs::exists("Server.cfg")) {
// parse it (this is weird and bad and should be removed in some future version)
ParseOldFormat();
}
} catch (const std::exception& e) {
beammp_error("an error occurred and was ignored during config transfer: " + std::string(e.what()));
if (mDisableConfig) {
return;
}
FlushToFile();
}
void TConfig::TryReadValue(toml::value& Table, const std::string& Category, const std::string_view& Key, const std::string_view& Env, std::string& OutValue) {
if (!Env.empty()) {
if (const char* envp = std::getenv(Env.data()); envp != nullptr && std::strcmp(envp, "") != 0) {
OutValue = std::string(envp);
return;
}
}
if (Table[Category.c_str()][Key.data()].is_string()) {
OutValue = Table[Category.c_str()][Key.data()].as_string();
}
}
// This arcane template magic is needed for using lambdas as overloaded visitors
// See https://en.cppreference.com/w/cpp/utility/variant/visit for reference
template <class... Ts>
struct overloaded : Ts... {
using Ts::operator()...;
};
template <class... Ts>
overloaded(Ts...) -> overloaded<Ts...>;
void TConfig::TryReadValue(toml::value& Table, const std::string& Category, const std::string_view& Key, const std::string_view& Env, bool& OutValue) {
void TConfig::TryReadValue(toml::value& Table, const std::string& Category, const std::string_view& Key, const std::string_view& Env, Settings::Key key) {
if (!Env.empty()) {
if (const char* envp = std::getenv(Env.data()); envp != nullptr && std::strcmp(envp, "") != 0) {
auto Str = std::string(envp);
OutValue = Str == "1" || Str == "true";
return;
}
}
if (Table[Category.c_str()][Key.data()].is_boolean()) {
OutValue = Table[Category.c_str()][Key.data()].as_boolean();
}
}
if (const char* envp = std::getenv(Env.data());
envp != nullptr && std::strcmp(envp, "") != 0) {
void TConfig::TryReadValue(toml::value& Table, const std::string& Category, const std::string_view& Key, const std::string_view& Env, int& OutValue) {
if (!Env.empty()) {
if (const char* envp = std::getenv(Env.data()); envp != nullptr && std::strcmp(envp, "") != 0) {
OutValue = int(std::strtol(envp, nullptr, 10));
std::visit(
overloaded {
[&envp, &key](std::string) {
Application::Settings.set(key, std::string(envp));
},
[&envp, &key](int) {
Application::Settings.set(key, int(std::strtol(envp, nullptr, 10)));
},
[&envp, &key](bool) {
auto Str = std::string(envp);
Application::Settings.set(key, bool(Str == "1" || Str == "true"));
} },
Application::Settings.get(key));
return;
}
}
if (Table[Category.c_str()][Key.data()].is_integer()) {
OutValue = int(Table[Category.c_str()][Key.data()].as_integer());
}
std::visit([&Table, &Category, &Key, &key](auto&& arg) {
using T = std::decay_t<decltype(arg)>;
if constexpr (std::is_same_v<T, std::string>) {
if (Table[Category.c_str()][Key.data()].is_string())
Application::Settings.set(key, Table[Category.c_str()][Key.data()].as_string());
else
beammp_warnf("Value '{}.{}' has unexpected type, expected type 'string'", Category, Key);
} else if constexpr (std::is_same_v<T, int>) {
if (Table[Category.c_str()][Key.data()].is_integer())
Application::Settings.set(key, int(Table[Category.c_str()][Key.data()].as_integer()));
else
beammp_warnf("Value '{}.{}' has unexpected type, expected type 'integer'", Category, Key);
} else if constexpr (std::is_same_v<T, bool>) {
if (Table[Category.c_str()][Key.data()].is_boolean())
Application::Settings.set(key, Table[Category.c_str()][Key.data()].as_boolean());
else
beammp_warnf("Value '{}.{}' has unexpected type, expected type 'boolean'", Category, Key);
} else {
throw std::logic_error { "Invalid type for config value during read attempt" };
}
},
Application::Settings.get(key));
}
void TConfig::ParseFromFile(std::string_view name) {
try {
toml::value data = toml::parse<toml::preserve_comments>(name.data());
toml::value data {};
if (!mDisableConfig) {
data = toml::parse(name.data());
}
// GENERAL
TryReadValue(data, "General", StrDebug, EnvStrDebug, Application::Settings.DebugModeEnabled);
TryReadValue(data, "General", StrPrivate, EnvStrPrivate, Application::Settings.Private);
TryReadValue(data, "General", StrPort, EnvStrPort, Application::Settings.Port);
TryReadValue(data, "General", StrMaxCars, EnvStrMaxCars, Application::Settings.MaxCars);
TryReadValue(data, "General", StrMaxPlayers, EnvStrMaxPlayers, Application::Settings.MaxPlayers);
TryReadValue(data, "General", StrMap, EnvStrMap, Application::Settings.MapName);
TryReadValue(data, "General", StrName, EnvStrName, Application::Settings.ServerName);
TryReadValue(data, "General", StrDescription, EnvStrDescription, Application::Settings.ServerDesc);
TryReadValue(data, "General", StrTags, EnvStrTags, Application::Settings.ServerTags);
TryReadValue(data, "General", StrResourceFolder, EnvStrResourceFolder, Application::Settings.Resource);
TryReadValue(data, "General", StrAuthKey, EnvStrAuthKey, Application::Settings.Key);
TryReadValue(data, "General", StrLogChat, EnvStrLogChat, Application::Settings.LogChat);
TryReadValue(data, "General", StrPassword, "", Application::Settings.Password);
// Read into new Settings Singleton
TryReadValue(data, "General", StrDebug, EnvStrDebug, Settings::Key::General_Debug);
TryReadValue(data, "General", StrPrivate, EnvStrPrivate, Settings::Key::General_Private);
TryReadValue(data, "General", StrInformationPacket, EnvStrInformationPacket, Settings::Key::General_InformationPacket);
if (Env::Get(Env::Key::PROVIDER_PORT_ENV).has_value()) {
TryReadValue(data, "General", StrPort, Env::Get(Env::Key::PROVIDER_PORT_ENV).value(), Settings::Key::General_Port);
} else {
TryReadValue(data, "General", StrPort, EnvStrPort, Settings::Key::General_Port);
}
if (Env::Get(Env::Key::PROVIDER_IP_ENV).has_value()) {
TryReadValue(data, "General", StrIP, Env::Get(Env::Key::PROVIDER_IP_ENV).value(), Settings::Key::General_IP);
} else {
TryReadValue(data, "General", StrIP, EnvStrIP, Settings::Key::General_IP);
}
TryReadValue(data, "General", StrMaxCars, EnvStrMaxCars, Settings::Key::General_MaxCars);
TryReadValue(data, "General", StrMaxPlayers, EnvStrMaxPlayers, Settings::Key::General_MaxPlayers);
TryReadValue(data, "General", StrMap, EnvStrMap, Settings::Key::General_Map);
TryReadValue(data, "General", StrName, EnvStrName, Settings::Key::General_Name);
TryReadValue(data, "General", StrDescription, EnvStrDescription, Settings::Key::General_Description);
TryReadValue(data, "General", StrTags, EnvStrTags, Settings::Key::General_Tags);
TryReadValue(data, "General", StrResourceFolder, EnvStrResourceFolder, Settings::Key::General_ResourceFolder);
TryReadValue(data, "General", StrAuthKey, EnvStrAuthKey, Settings::Key::General_AuthKey);
TryReadValue(data, "General", StrLogChat, EnvStrLogChat, Settings::Key::General_LogChat);
TryReadValue(data, "General", StrAllowGuests, EnvStrAllowGuests, Settings::Key::General_AllowGuests);
// Misc
TryReadValue(data, "Misc", StrSendErrors, "", Application::Settings.SendErrors);
TryReadValue(data, "Misc", StrHideUpdateMessages, "", Application::Settings.HideUpdateMessages);
TryReadValue(data, "Misc", StrSendErrorsMessageEnabled, "", Application::Settings.SendErrorsMessageEnabled);
TryReadValue(data, "Misc", StrHideUpdateMessages, EnvStrHideUpdateMessages, Settings::Key::Misc_ImScaredOfUpdates);
TryReadValue(data, "Misc", StrUpdateReminderTime, EnvStrUpdateReminderTime, Settings::Key::Misc_UpdateReminderTime);
} catch (const std::exception& err) {
beammp_error("Error parsing config file value: " + std::string(err.what()));
mFailed = true;
Application::SetSubsystemStatus("Config", Application::Status::Bad);
return;
}
PrintDebug();
// Update in any case
FlushToFile();
if (!mDisableConfig) {
FlushToFile();
}
// all good so far, let's check if there's a key
if (Application::Settings.Key.empty()) {
beammp_error("No AuthKey specified in the \"" + std::string(mConfigFileName) + "\" file. Please get an AuthKey, enter it into the config file, and restart this server.");
if (Application::Settings.getAsString(Settings::Key::General_AuthKey).empty()) {
if (mDisableConfig) {
beammp_error("No AuthKey specified in the environment.");
} else {
beammp_error("No AuthKey specified in the \"" + std::string(mConfigFileName) + "\" file. Please get an AuthKey, enter it into the config file, and restart this server.");
}
Application::SetSubsystemStatus("Config", Application::Status::Bad);
mFailed = true;
return;
}
Application::SetSubsystemStatus("Config", Application::Status::Good);
if (Application::Settings.Key.size() != 36) {
if (Application::Settings.getAsString(Settings::Key::General_AuthKey).size() != 36) {
beammp_warn("AuthKey specified is the wrong length and likely isn't valid.");
}
}
void TConfig::PrintDebug() {
beammp_debug(std::string(StrDebug) + ": " + std::string(Application::Settings.DebugModeEnabled ? "true" : "false"));
beammp_debug(std::string(StrPrivate) + ": " + std::string(Application::Settings.Private ? "true" : "false"));
beammp_debug(std::string(StrPort) + ": " + std::to_string(Application::Settings.Port));
beammp_debug(std::string(StrMaxCars) + ": " + std::to_string(Application::Settings.MaxCars));
beammp_debug(std::string(StrMaxPlayers) + ": " + std::to_string(Application::Settings.MaxPlayers));
beammp_debug(std::string(StrMap) + ": \"" + Application::Settings.MapName + "\"");
beammp_debug(std::string(StrName) + ": \"" + Application::Settings.ServerName + "\"");
beammp_debug(std::string(StrDescription) + ": \"" + Application::Settings.ServerDesc + "\"");
if (mDisableConfig) {
beammp_debug("Provider turned off the generation and parsing of the ServerConfig.toml");
}
beammp_debug(std::string(StrDebug) + ": " + std::string(Application::Settings.getAsBool(Settings::Key::General_Debug) ? "true" : "false"));
beammp_debug(std::string(StrPrivate) + ": " + std::string(Application::Settings.getAsBool(Settings::Key::General_Private) ? "true" : "false"));
beammp_debug(std::string(StrInformationPacket) + ": " + std::string(Application::Settings.getAsBool(Settings::Key::General_InformationPacket) ? "true" : "false"));
beammp_debug(std::string(StrPort) + ": " + std::to_string(Application::Settings.getAsInt(Settings::Key::General_Port)));
beammp_debug(std::string(StrIP) + ": \"" + Application::Settings.getAsString(Settings::Key::General_IP) + "\"");
beammp_debug(std::string(StrMaxCars) + ": " + std::to_string(Application::Settings.getAsInt(Settings::Key::General_MaxCars)));
beammp_debug(std::string(StrMaxPlayers) + ": " + std::to_string(Application::Settings.getAsInt(Settings::Key::General_MaxPlayers)));
beammp_debug(std::string(StrMap) + ": \"" + Application::Settings.getAsString(Settings::Key::General_Map) + "\"");
beammp_debug(std::string(StrName) + ": \"" + Application::Settings.getAsString(Settings::Key::General_Name) + "\"");
beammp_debug(std::string(StrDescription) + ": \"" + Application::Settings.getAsString(Settings::Key::General_Description) + "\"");
beammp_debug(std::string(StrTags) + ": " + TagsAsPrettyArray());
beammp_debug(std::string(StrLogChat) + ": \"" + (Application::Settings.LogChat ? "true" : "false") + "\"");
beammp_debug(std::string(StrResourceFolder) + ": \"" + Application::Settings.Resource + "\"");
beammp_debug(std::string(StrLogChat) + ": \"" + (Application::Settings.getAsBool(Settings::Key::General_LogChat) ? "true" : "false") + "\"");
beammp_debug(std::string(StrResourceFolder) + ": \"" + Application::Settings.getAsString(Settings::Key::General_ResourceFolder) + "\"");
beammp_debug(std::string(StrAllowGuests) + ": \"" + (Application::Settings.getAsBool(Settings::Key::General_AllowGuests) ? "true" : "false") + "\"");
// special!
beammp_debug("Key Length: " + std::to_string(Application::Settings.Key.length()) + "");
beammp_debug("Password Protected: " + std::string(Application::Settings.Password.empty() ? "false" : "true"));
beammp_debug("Key Length: " + std::to_string(Application::Settings.getAsString(Settings::Key::General_AuthKey).length()) + "");
}
void TConfig::ParseOldFormat() {
std::ifstream File("Server.cfg");
// read all, strip comments
std::string Content;
for (;;) {
std::string Line;
std::getline(File, Line);
if (!Line.empty() && Line.at(0) != '#') {
Line = Line.substr(0, Line.find_first_of('#'));
Content += Line + "\n";
}
if (!File.good()) {
break;
}
}
std::stringstream Str(Content);
std::string Key, Ignore, Value;
for (;;) {
Str >> Key >> std::ws >> Ignore >> std::ws;
std::getline(Str, Value);
if (Str.eof()) {
break;
}
std::stringstream ValueStream(Value);
ValueStream >> std::ws; // strip leading whitespace if any
Value = ValueStream.str();
if (Key == "Debug") {
Application::Settings.DebugModeEnabled = Value.find("true") != std::string::npos;
} else if (Key == "Private") {
Application::Settings.Private = Value.find("true") != std::string::npos;
} else if (Key == "Port") {
ValueStream >> Application::Settings.Port;
} else if (Key == "Cars") {
ValueStream >> Application::Settings.MaxCars;
} else if (Key == "MaxPlayers") {
ValueStream >> Application::Settings.MaxPlayers;
} else if (Key == "Map") {
Application::Settings.MapName = Value.substr(1, Value.size() - 3);
} else if (Key == "Name") {
Application::Settings.ServerName = Value.substr(1, Value.size() - 3);
} else if (Key == "Desc") {
Application::Settings.ServerDesc = Value.substr(1, Value.size() - 3);
} else if (Key == "use") {
Application::Settings.Resource = Value.substr(1, Value.size() - 3);
} else if (Key == "AuthKey") {
Application::Settings.Key = Value.substr(1, Value.size() - 3);
} else {
beammp_warn("unknown key in old auth file (ignored): " + Key);
}
Str >> std::ws;
}
}
std::string TConfig::TagsAsPrettyArray() const {
std::vector<std::string> TagsArray = {};
SplitString(Application::Settings.ServerTags, ',', TagsArray);
SplitString(Application::Settings.getAsString(Settings::General_Tags), ',', TagsArray);
std::string Pretty = {};
for (size_t i = 0; i < TagsArray.size() - 1; ++i) {
Pretty += '\"' + TagsArray[i] + "\", ";
+285 -98
View File
@@ -20,12 +20,19 @@
#include "Common.h"
#include "Compat.h"
#include "Client.h"
#include "CustomAssert.h"
#include "LuaAPI.h"
#include "TLuaEngine.h"
#include "Http.h"
#include <ctime>
#include <lua.hpp>
#include <mutex>
#include <openssl/opensslv.h>
#include <sstream>
#include <stdexcept>
#include <unordered_map>
static inline bool StringStartsWith(const std::string& What, const std::string& StartsWith) {
return What.size() >= StartsWith.size() && What.substr(0, StartsWith.size()) == StartsWith;
@@ -74,7 +81,7 @@ static std::string GetDate() {
auto local_tm = std::localtime(&tt);
char buf[30];
std::string date;
if (Application::Settings.DebugModeEnabled) {
if (Application::Settings.getAsBool(Settings::Key::General_Debug)) {
std::strftime(buf, sizeof(buf), "[%d/%m/%y %T.", local_tm);
date += buf;
auto seconds = std::chrono::time_point_cast<std::chrono::seconds>(now);
@@ -102,41 +109,6 @@ void TConsole::BackupOldLog() {
} catch (const std::exception& e) {
beammp_warn(e.what());
}
/*
int err = 0;
zip* z = zip_open("ServerLogs.zip", ZIP_CREATE, &err);
if (!z) {
std::cerr << GetPlatformAgnosticErrorString() << std::endl;
return;
}
FILE* File = std::fopen(Path.string().c_str(), "r");
if (!File) {
std::cerr << GetPlatformAgnosticErrorString() << std::endl;
return;
}
std::vector<uint8_t> Buffer;
Buffer.resize(fs::file_size(Path));
std::fread(Buffer.data(), 1, Buffer.size(), File);
std::fclose(File);
auto s = zip_source_buffer(z, Buffer.data(), Buffer.size(), 0);
auto TimePoint = fs::last_write_time(Path);
auto Secs = TimePoint.time_since_epoch().count();
auto MyTimeT = std::time(&Secs);
std::string NewName = Path.stem().string();
NewName += "_";
std::string Time;
Time.resize(32);
size_t n = strftime(Time.data(), Time.size(), "%F_%H.%M.%S", localtime(&MyTimeT));
Time.resize(n);
NewName += Time;
NewName += ".log";
zip_file_add(z, NewName.c_str(), s, 0);
zip_close(z);
*/
}
}
@@ -153,7 +125,6 @@ void TConsole::StartLoggingToFile() {
void TConsole::ChangeToLuaConsole(const std::string& LuaStateId) {
if (!mIsLuaConsole) {
/*
if (!mLuaEngine) {
beammp_error("Lua engine not initialized yet, please wait and try again");
return;
@@ -169,7 +140,7 @@ void TConsole::ChangeToLuaConsole(const std::string& LuaStateId) {
mCommandline->set_prompt("lua> ");
}
mCachedRegularHistory = mCommandline->history();
mCommandline->set_history(mCachedLuaHistory);*/
mCommandline->set_history(mCachedLuaHistory);
}
}
@@ -189,7 +160,7 @@ void TConsole::ChangeToRegularConsole() {
}
bool TConsole::EnsureArgsCount(const std::vector<std::string>& args, size_t n) {
if (n == 0 && args.size() != 0) {
if (n == 0 && !args.empty()) {
Application::Console().WriteRaw("This command expects no arguments.");
return false;
} else if (args.size() != n) {
@@ -222,17 +193,13 @@ void TConsole::Command_Lua(const std::string&, const std::vector<std::string>& a
if (args.size() == 1) {
auto NewStateId = args.at(0);
beammp_assert(!NewStateId.empty());
// TODO: This must be implemented
/*
if (mLuaEngine->HasState(NewStateId)) {
ChangeToLuaConsole(NewStateId);
} else {
Application::Console().WriteRaw("Lua state '" + NewStateId + "' is not a known state. Didn't switch to Lua.");
}
*/
} else if (args.size() == 0) {
} else if (args.empty()) {
ChangeToLuaConsole(mDefaultStateId);
// CONTINUE HERE
}
}
@@ -242,15 +209,18 @@ void TConsole::Command_Help(const std::string&, const std::vector<std::string>&
}
static constexpr const char* sHelpString = R"(
Commands:
help displays this help
exit shuts down the server
kick <name> [reason] kicks specified player with an optional reason
list lists all players and info about them
say <message> sends the message to all players in chat
lua [state id] switches to lua, optionally into a specific state id's lua
settings [command] sets or gets settings for the server, run `settings help` for more info
status how the server is doing and what it's up to
clear clears the console window)";
help displays this help
exit shuts down the server
kick <name> [reason] kicks specified player with an optional reason
list lists all players and info about them
say <message> sends the message to all players in chat
lua [state id] switches to lua, optionally into a specific state id's lua
settings [command] sets or gets settings for the server, run `settings help` for more info
status how the server is doing and what it's up to
clear clears the console window
version displays the server version
protectmod <name> <value> sets whether a mod is protected, value can be true or false
reloadmods reloads all mods from the Resources Client folder)";
Application::Console().WriteRaw("BeamMP-Server Console: " + std::string(sHelpString));
}
@@ -270,6 +240,82 @@ void TConsole::Command_Clear(const std::string&, const std::vector<std::string>&
mCommandline->write("\x1b[;H\x1b[2J");
}
void TConsole::Command_Version(const std::string& cmd, const std::vector<std::string>& args) {
if (!EnsureArgsCount(args, 0)) {
return;
}
std::string platform;
#if defined(BEAMMP_WINDOWS)
platform = "Windows";
#elif defined(BEAMMP_LINUX)
platform = "Linux";
#elif defined(BEAMMP_FREEBSD)
platform = "FreeBSD";
#elif defined(BEAMMP_APPLE)
platform = "Apple";
#else
platform = "Unknown";
#endif
Application::Console().WriteRaw("Platform: " + platform);
Application::Console().WriteRaw("Server: v" + Application::ServerVersionString());
std::string lua_version = fmt::format("Lua: v{}.{}.{}", LUA_VERSION_MAJOR, LUA_VERSION_MINOR, LUA_VERSION_RELEASE);
Application::Console().WriteRaw(lua_version);
std::string openssl_version = fmt::format("OpenSSL: v{}.{}.{}", OPENSSL_VERSION_MAJOR, OPENSSL_VERSION_MINOR, OPENSSL_VERSION_PATCH);
Application::Console().WriteRaw(openssl_version);
}
void TConsole::Command_ProtectMod(const std::string& cmd, const std::vector<std::string>& args) {
if (!EnsureArgsCount(args, 2)) {
return;
}
const auto& ModName = args.at(0);
const auto& Protect = args.at(1);
for (auto mod : mLuaEngine->Network().ResourceManager().GetMods()) {
if (mod["file_name"].get<std::string>() == ModName) {
mLuaEngine->Network().ResourceManager().SetProtected(ModName, Protect == "true");
Application::Console().WriteRaw("Mod " + ModName + " is now " + (Protect == "true" ? "protected" : "unprotected"));
return;
}
}
Application::Console().WriteRaw("Mod " + ModName + " not found.");
}
void TConsole::Command_ReloadMods(const std::string& cmd, const std::vector<std::string>& args) {
if (!EnsureArgsCount(args, 0)) {
return;
}
mLuaEngine->Network().ResourceManager().RefreshFiles();
Application::Console().WriteRaw("Mods reloaded.");
}
void TConsole::Command_NetTest(const std::string& cmd, const std::vector<std::string>& args) {
unsigned int status = 0;
std::string T = Http::GET(
Application::GetServerCheckUrl() + "/api/v2/beammp/" + std::to_string(Application::Settings.getAsInt(Settings::Key::General_Port)), &status
);
beammp_debugf("Status and response from Server Check API: {0}, {1}", status, T);
auto Doc = nlohmann::json::parse(T, nullptr, false);
if (Doc.is_discarded() || !Doc.is_object()) {
beammp_warn("Failed to parse Server Check API response, however the server will most likely still work correctly.");
} else {
std::string status = Doc["status"];
std::string details = "Response from Server Check API: " + std::string(Doc["details"]);
if (status == "ok") {
beammp_info(details);
} else {
beammp_warn(details);
}
}
}
void TConsole::Command_Kick(const std::string&, const std::vector<std::string>& args) {
if (!EnsureArgsCount(args, 1, size_t(-1))) {
return;
@@ -287,15 +333,17 @@ void TConsole::Command_Kick(const std::string&, const std::vector<std::string>&
std::for_each(Name2.begin(), Name2.end(), [](char& c) { c = char(std::tolower(char(c))); });
return StringStartsWith(Name1, Name2) || StringStartsWith(Name2, Name1);
};
throw std::runtime_error(fmt::format("NOT IMPLEMENTED: {}", __func__));
/*mLuaEngine->Server().ForEachClient([&](const std::shared_ptr<TClient>& Client) -> bool {
if (NameCompare(Client->Name.get(), Name)) {
mLuaEngine->Network().ClientKick(*Client, Reason);
mLuaEngine->Server().ForEachClient([&](std::weak_ptr<TClient> Client) -> bool {
if (!Client.expired()) {
auto locked = Client.lock();
if (NameCompare(locked->GetName(), Name)) {
mLuaEngine->Network().ClientKick(*locked, Reason);
Kicked = true;
return false;
}
}
return true;
});*/
});
if (!Kicked) {
Application::Console().WriteRaw("Error: No player with name matching '" + Name + "' was found.");
} else {
@@ -358,8 +406,142 @@ std::tuple<std::string, std::vector<std::string>> TConsole::ParseCommand(const s
return { Command, Args };
}
template <class... Ts>
struct overloaded : Ts... {
using Ts::operator()...;
};
template <class... Ts>
overloaded(Ts...) -> overloaded<Ts...>;
void TConsole::Command_Settings(const std::string&, const std::vector<std::string>& args) {
if (!EnsureArgsCount(args, 1, 2)) {
static constexpr const char* sHelpString = R"(
Settings:
settings help displays this help
settings list lists all settings
settings get <category> <setting> prints current value of specified setting
settings set <category> <setting> <value> sets specified setting to value
)";
if (args.empty()) {
beammp_errorf("No arguments specified for command 'settings'!");
Application::Console().WriteRaw("BeamMP-Server Console: " + std::string(sHelpString));
return;
}
if (args.front() == "help") {
Application::Console().WriteRaw("BeamMP-Server Console: " + std::string(sHelpString));
return;
} else if (args.front() == "get") {
if (args.size() < 3) {
beammp_errorf("'settings get' needs at least two arguments!");
Application::Console().WriteRaw("BeamMP-Server Console: " + std::string(sHelpString));
return;
}
try {
Settings::SettingsAccessControl acl = Application::Settings.getConsoleInputAccessMapping(ComposedKey { args.at(1), args.at(2) });
Settings::SettingsTypeVariant keyType = Application::Settings.get(acl.first);
std::visit(
overloaded {
[&args](std::string keyValue) {
Application::Console().WriteRaw(fmt::format("'{}::{}' = {}", args.at(1), args.at(2), keyValue));
},
[&args](int keyValue) {
Application::Console().WriteRaw(fmt::format("'{}::{}' = {}", args.at(1), args.at(2), keyValue));
},
[&args](bool keyValue) {
Application::Console().WriteRaw(fmt::format("'{}::{}' = {}", args.at(1), args.at(2), keyValue));
}
},
keyType);
} catch (std::logic_error& e) {
beammp_errorf("Error when getting key: {}", e.what());
return;
}
} else if (args.front() == "set") {
if (args.size() <= 3) {
beammp_errorf("'settings set' needs at least three arguments!");
Application::Console().WriteRaw("BeamMP-Server Console: " + std::string(sHelpString));
return;
}
try {
Settings::SettingsAccessControl acl = Application::Settings.getConsoleInputAccessMapping(ComposedKey { args.at(1), args.at(2) });
Settings::SettingsTypeVariant keyType = Application::Settings.get(acl.first);
std::visit(
overloaded {
[&args](std::string keyValue) {
Application::Settings.setConsoleInputAccessMapping(ComposedKey { args.at(1), args.at(2) }, std::string(args.at(3)));
Application::Console().WriteRaw(fmt::format("{}::{} := {}", args.at(1), args.at(2), std::string(args.at(3))));
},
[&args](int keyValue) {
Application::Settings.setConsoleInputAccessMapping(ComposedKey { args.at(1), args.at(2) }, std::stoi(args.at(3)));
Application::Console().WriteRaw(fmt::format("{}::{} := {}", args.at(1), args.at(2), std::stoi(args.at(3))));
},
[&args](bool keyValue) {
if (args.at(3) == "true") {
Application::Settings.setConsoleInputAccessMapping(ComposedKey { args.at(1), args.at(2) }, true);
Application::Console().WriteRaw(fmt::format("{}::{} := {}", args.at(1), args.at(2), "true"));
} else if (args.at(3) == "false") {
Application::Settings.setConsoleInputAccessMapping(ComposedKey { args.at(1), args.at(2) }, false);
Application::Console().WriteRaw(fmt::format("{}::{} := {}", args.at(1), args.at(2), "false"));
} else {
beammp_errorf("Error when setting key: {}::{} : Unknown literal, use either 'true', or 'false' to set boolean values.", args.at(1), args.at(2));
}
}
},
keyType);
} catch (std::logic_error& e) {
beammp_errorf("Exception when setting settings key via console: {}", e.what());
return;
}
} else if (args.front() == "list") {
for (const auto& [composedKey, keyACL] : Application::Settings.getAccessControlMap()) {
// even though we have the value, we want to ignore it in order to make use of access
// control checks
if (keyACL.second != Settings::SettingsAccessMask::NO_ACCESS) {
try {
Settings::SettingsAccessControl acl = Application::Settings.getConsoleInputAccessMapping(composedKey);
Settings::SettingsTypeVariant keyType = Application::Settings.get(acl.first);
std::visit(
overloaded {
[&composedKey](std::string keyValue) {
Application::Console().WriteRaw(fmt::format("{} = {}", composedKey, keyValue));
},
[&composedKey](int keyValue) {
Application::Console().WriteRaw(fmt::format("{} = {}", composedKey, keyValue));
},
[&composedKey](bool keyValue) {
Application::Console().WriteRaw(fmt::format("{} = {}", composedKey, keyValue));
}
},
keyType);
} catch (std::logic_error& e) {
beammp_errorf("Error when getting key: {}", e.what());
}
}
}
} else {
beammp_errorf("Unknown argument for command 'settings': {}", args.front());
Application::Console().WriteRaw("BeamMP-Server Console: " + std::string(sHelpString));
return;
}
}
@@ -368,7 +550,7 @@ void TConsole::Command_Say(const std::string& FullCmd) {
if (FullCmd.size() > 3) {
auto Message = FullCmd.substr(4);
LuaAPI::MP::SendChatMessage(-1, Message);
if (!Application::Settings.LogChat) {
if (!Application::Settings.getAsBool(Settings::Key::General_LogChat)) {
Application::Console().WriteRaw("Chat message sent!");
}
}
@@ -378,24 +560,23 @@ void TConsole::Command_List(const std::string&, const std::vector<std::string>&
if (!EnsureArgsCount(args, 0)) {
return;
}
// Implement once running
throw std::runtime_error(fmt::format("NOT IMPLEMENTED: {}", __func__));
/*
if (mLuaEngine->Server().ClientCount() == 0) {
Application::Console().WriteRaw("No players online.");
} else {
std::stringstream ss;
ss << std::left << std::setw(25) << "Name" << std::setw(6) << "ID" << std::setw(6) << "Cars" << std::endl;
mLuaEngine->Server().ForEachClient([&](const std::shared_ptr<TClient>& Client) -> bool {
ss << std::left << std::setw(25) << Client->Name.get()
<< std::setw(6) << Client->ID.get()
<< std::setw(6) << Client->GetCarCount() << "\n";
mLuaEngine->Server().ForEachClient([&](std::weak_ptr<TClient> Client) -> bool {
if (!Client.expired()) {
auto locked = Client.lock();
ss << std::left << std::setw(25) << locked->GetName()
<< std::setw(6) << locked->GetID()
<< std::setw(6) << locked->GetCarCount() << "\n";
}
return true;
});
auto Str = ss.str();
Application::Console().WriteRaw(Str.substr(0, Str.size() - 1));
}
*/
}
void TConsole::Command_Status(const std::string&, const std::vector<std::string>& args) {
@@ -404,6 +585,29 @@ void TConsole::Command_Status(const std::string&, const std::vector<std::string>
}
std::stringstream Status;
size_t CarCount = 0;
size_t ConnectedCount = 0;
size_t GuestCount = 0;
size_t SyncedCount = 0;
size_t SyncingCount = 0;
size_t MissedPacketQueueSum = 0;
int LargestSecondsSinceLastPing = 0;
mLuaEngine->Server().ForEachClient([&](std::weak_ptr<TClient> Client) -> bool {
if (!Client.expired()) {
auto Locked = Client.lock();
CarCount += Locked->GetCarCount();
ConnectedCount += Locked->IsUDPConnected() ? 1 : 0;
GuestCount += Locked->IsGuest() ? 1 : 0;
SyncedCount += Locked->IsSynced() ? 1 : 0;
SyncingCount += Locked->IsSyncing() ? 1 : 0;
MissedPacketQueueSum += Locked->MissedPacketQueueSize();
if (Locked->SecondsSinceLastPing() < LargestSecondsSinceLastPing) {
LargestSecondsSinceLastPing = Locked->SecondsSinceLastPing();
}
}
return true;
});
size_t SystemsStarting = 0;
size_t SystemsGood = 0;
size_t SystemsBad = 0;
@@ -448,22 +652,15 @@ void TConsole::Command_Status(const std::string&, const std::vector<std::string>
SystemsShuttingDownList = SystemsShuttingDownList.substr(0, SystemsShuttingDownList.size() - 2);
SystemsShutdownList = SystemsShutdownList.substr(0, SystemsShutdownList.size() - 2);
auto ElapsedTime = mUptimeTimer.GetElapsedTime();
/*
auto network = mLuaEngine->Network();
auto clients = network->all_clients();
auto ElapsedTime = mLuaEngine->Server().UptimeTimer.GetElapsedTime();
Status << "BeamMP-Server Status:\n"
<< "\tTotal Players: " << clients.size() << "\n"
<< "\tPlayers identifying: " << network->clients_in_state_count(bmp::State::Identification) << "\n"
<< "\tPlayers authenticating: " << network->clients_in_state_count(bmp::State::Authentication) << "\n"
<< "\tPlayers downloading mods: " << network->clients_in_state_count(bmp::State::ModDownload) << "\n"
<< "\tPlayers spawning in: " << network->clients_in_state_count(bmp::State::SessionSetup) << "\n"
<< "\tPlayers playing: " << network->clients_in_state_count(bmp::State::Playing) << "\n"
<< "\tPlayers leaving: " << network->clients_in_state_count(bmp::State::Leaving) << "\n"
<< "\tGuests: " << network->guest_count() << "\n"
<< "\tVehicles: " << network->vehicle_count() << "\n"
<< "\tTotal Players: " << mLuaEngine->Server().ClientCount() << "\n"
<< "\tSyncing Players: " << SyncingCount << "\n"
<< "\tSynced Players: " << SyncedCount << "\n"
<< "\tConnected Players: " << ConnectedCount << "\n"
<< "\tGuests: " << GuestCount << "\n"
<< "\tCars: " << CarCount << "\n"
<< "\tUptime: " << ElapsedTime << "ms (~" << size_t(double(ElapsedTime) / 1000.0 / 60.0 / 60.0) << "h) \n"
<< "\tLua:\n"
<< "\t\tQueued results to check: " << mLuaEngine->GetResultsToCheckSize() << "\n"
@@ -478,13 +675,12 @@ void TConsole::Command_Status(const std::string&, const std::vector<std::string>
<< "\t\tBad: [ " << SystemsBadList << " ]\n"
<< "\t\tShutting down: [ " << SystemsShuttingDownList << " ]\n"
<< "\t\tShut down: [ " << SystemsShutdownList << " ]\n"
<< "";*/
<< "";
Application::Console().WriteRaw(Status.str());
}
void TConsole::RunAsCommand(const std::string& cmd, bool IgnoreNotACommand) {
/*
auto FutureIsNonNil =
[](const std::shared_ptr<TLuaResult>& Future) {
if (!Future->Error && Future->Result.valid()) {
@@ -509,7 +705,7 @@ void TConsole::RunAsCommand(const std::string& cmd, bool IgnoreNotACommand) {
}
}
}
if (NonNilFutures.size() == 0) {
if (NonNilFutures.empty()) {
if (!IgnoreNotACommand) {
Application::Console().WriteRaw("Error: Unknown command: '" + cmd + "'. Type 'help' to see a list of valid commands.");
}
@@ -528,11 +724,9 @@ void TConsole::RunAsCommand(const std::string& cmd, bool IgnoreNotACommand) {
}
Application::Console().WriteRaw(Reply.str());
}
*/
}
void TConsole::HandleLuaInternalCommand(const std::string& cmd) {
/*
if (cmd == "exit") {
ChangeToRegularConsole();
} else if (cmd == "queued") {
@@ -582,7 +776,6 @@ Commands
} else {
beammp_error("internal command '" + cmd + "' is not known");
}
*/
}
TConsole::TConsole() {
@@ -601,7 +794,6 @@ void TConsole::InitializeCommandline() {
auto [cmd, args] = ParseCommand(TrimmedCmd);
mCommandline->write(mCommandline->prompt() + TrimmedCmd);
if (mIsLuaConsole) {
/*
if (!mLuaEngine) {
beammp_info("Lua not started yet, please try again in a second");
} else if (!cmd.empty() && cmd.at(0) == ':') {
@@ -613,9 +805,7 @@ void TConsole::InitializeCommandline() {
beammp_lua_error("error in " + mStateId + ": " + Future->ErrorMessage);
}
}
*/
} else {
/*
if (!mLuaEngine) {
beammp_error("Attempted to run a command before Lua engine started. Please wait and try again.");
} else if (cmd == "exit") {
@@ -631,7 +821,7 @@ void TConsole::InitializeCommandline() {
} else {
RunAsCommand(TrimmedCmd);
}
}*/
}
}
} catch (const std::exception& e) {
beammp_error("Console died with: " + std::string(e.what()) + ". This could be a fatal error and could cause the server to terminate.");
@@ -641,7 +831,6 @@ void TConsole::InitializeCommandline() {
std::vector<std::string> suggestions;
try {
if (mIsLuaConsole) { // if lua
/*
if (!mLuaEngine) {
beammp_info("Lua not started yet, please try again in a second");
} else {
@@ -663,6 +852,7 @@ void TConsole::InitializeCommandline() {
if (stub.rfind('.') != stub.size() - 1 && !tablekeys.empty()) {
tablekeys.pop_back();
}
auto keys = mLuaEngine->GetStateTableKeysForState(mStateId, tablekeys);
for (const auto& key : keys) { // go through each bottom-level key
@@ -678,18 +868,17 @@ void TConsole::InitializeCommandline() {
suggestions.push_back(prefix + before_last_atom + key);
}
}
}*/
}
} else { // if not lua
if (stub.find("lua") == 0) { // starts with "lua" means we should suggest state names
std::string after_prefix = TrimString(stub.substr(3));
/*auto stateNames = mLuaEngine->GetLuaStateNames();
auto stateNames = mLuaEngine->GetLuaStateNames();
for (const auto& name : stateNames) {
if (name.find(after_prefix) == 0) {
suggestions.push_back("lua " + name);
}
}
*/
} else {
for (const auto& [cmd_name, cmd_fn] : mCommandMap) {
if (cmd_name.find(stub) == 0) {
@@ -725,8 +914,6 @@ void TConsole::WriteRaw(const std::string& str) {
}
}
/*
void TConsole::InitializeLuaConsole(TLuaEngine& Engine) {
mLuaEngine = &Engine;
}
*/
+66 -51
View File
@@ -18,14 +18,14 @@
#include "THeartbeatThread.h"
#include "ChronoWrapper.h"
#include "Client.h"
#include "Common.h"
#include "Http.h"
// #include "SocketIO.h"
#include <rapidjson/document.h>
#include <rapidjson/rapidjson.h>
#include <nlohmann/json.hpp>
#include <sstream>
namespace json = rapidjson;
void THeartbeatThread::operator()() {
RegisterThread("Heartbeat");
std::string Body;
@@ -35,15 +35,17 @@ void THeartbeatThread::operator()() {
static std::string Last;
static std::chrono::high_resolution_clock::time_point LastNormalUpdateTime = std::chrono::high_resolution_clock::now();
static std::chrono::high_resolution_clock::time_point LastUpdateReminderTime = std::chrono::high_resolution_clock::now();
bool isAuth = false;
size_t UpdateReminderCounter = 0;
std::chrono::high_resolution_clock::duration UpdateReminderTimePassed;
while (!Application::IsShuttingDown()) {
++UpdateReminderCounter;
auto UpdateReminderTimeout = ChronoWrapper::TimeFromStringWithLiteral(Application::Settings.getAsString(Settings::Key::Misc_UpdateReminderTime));
Body = GenerateCall();
// a hot-change occurs when a setting has changed, to update the backend of that change.
auto Now = std::chrono::high_resolution_clock::now();
bool Unchanged = Last == Body;
auto TimePassed = (Now - LastNormalUpdateTime);
UpdateReminderTimePassed = (Now - LastUpdateReminderTime);
auto Threshold = Unchanged ? 30 : 5;
if (TimePassed < std::chrono::seconds(Threshold)) {
std::this_thread::sleep_for(std::chrono::milliseconds(100));
@@ -53,22 +55,23 @@ void THeartbeatThread::operator()() {
Last = Body;
LastNormalUpdateTime = Now;
if (!Application::Settings.CustomIP.empty()) {
Body += "&ip=" + Application::Settings.CustomIP;
}
auto Target = "/heartbeat";
unsigned int ResponseCode = 0;
json::Document Doc;
nlohmann::json Doc;
bool Ok = false;
for (const auto& Url : Application::GetBackendUrlsInOrder()) {
T = Http::POST(Url, 443, Target, Body, "application/x-www-form-urlencoded", &ResponseCode, { { "api-v", "2" } });
Doc.Parse(T.data(), T.size());
if (Doc.HasParseError() || !Doc.IsObject()) {
if (!Application::Settings.Private) {
T = Http::POST(Url + Target, Body, "application/json", &ResponseCode, { { "api-v", "2" } });
if (!Application::Settings.getAsBool(Settings::Key::General_Private)) {
beammp_debug("Backend response was: `" + T + "`");
}
Doc = nlohmann::json::parse(T, nullptr, false);
if (Doc.is_discarded() || !Doc.is_object()) {
if (!Application::Settings.getAsBool(Settings::Key::General_Private)) {
beammp_trace("Backend response failed to parse as valid json");
beammp_trace("Response was: `" + T + "`");
}
} else if (ResponseCode != 200) {
beammp_errorf("Response code from the heartbeat: {}", ResponseCode);
@@ -87,18 +90,18 @@ void THeartbeatThread::operator()() {
const auto MessageKey = "msg";
if (Ok) {
if (Doc.HasMember(StatusKey) && Doc[StatusKey].IsString()) {
Status = Doc[StatusKey].GetString();
if (Doc.contains(StatusKey) && Doc[StatusKey].is_string()) {
Status = Doc[StatusKey];
} else {
Ok = false;
}
if (Doc.HasMember(CodeKey) && Doc[CodeKey].IsString()) {
Code = Doc[CodeKey].GetString();
if (Doc.contains(CodeKey) && Doc[CodeKey].is_string()) {
Code = Doc[CodeKey];
} else {
Ok = false;
}
if (Doc.HasMember(MessageKey) && Doc[MessageKey].IsString()) {
Message = Doc[MessageKey].GetString();
if (Doc.contains(MessageKey) && Doc[MessageKey].is_string()) {
Message = Doc[MessageKey];
} else {
Ok = false;
}
@@ -106,12 +109,12 @@ void THeartbeatThread::operator()() {
beammp_error("Missing/invalid json members in backend response");
}
} else {
if (!Application::Settings.Private) {
if (!Application::Settings.getAsBool(Settings::Key::General_Private)) {
beammp_warn("Backend failed to respond to a heartbeat. Your server may temporarily disappear from the server list. This is not an error, and will likely resolve itself soon. Direct connect will still work.");
}
}
if (Ok && !isAuth && !Application::Settings.Private) {
if (Ok && !isAuth && !Application::Settings.getAsBool(Settings::Key::General_Private)) {
if (Status == "2000") {
beammp_info(("Authenticated! " + Message));
isAuth = true;
@@ -125,38 +128,45 @@ void THeartbeatThread::operator()() {
beammp_error("Backend REFUSED the auth key. Reason: " + Message);
}
}
if (isAuth || Application::Settings.Private) {
if (isAuth || Application::Settings.getAsBool(Settings::Key::General_Private)) {
Application::SetSubsystemStatus("Heartbeat", Application::Status::Good);
}
if (!Application::Settings.HideUpdateMessages && UpdateReminderCounter % 5) {
if (!Application::Settings.getAsBool(Settings::Key::Misc_ImScaredOfUpdates) && UpdateReminderTimePassed.count() > UpdateReminderTimeout.count()) {
LastUpdateReminderTime = std::chrono::high_resolution_clock::now();
Application::CheckForUpdates();
}
}
}
std::string THeartbeatThread::GenerateCall() {
std::stringstream Ret;
nlohmann::json Ret = {
{ "players", std::to_string(mServer.ClientCount()) },
{ "maxplayers", std::to_string(Application::Settings.getAsInt(Settings::Key::General_MaxPlayers)) },
{ "port", std::to_string(Application::Settings.getAsInt(Settings::Key::General_Port)) },
{ "map", Application::Settings.getAsString(Settings::Key::General_Map) },
{ "private", Application::Settings.getAsBool(Settings::Key::General_Private) ? "true" : "false" },
{ "version", Application::ServerVersionString() },
{ "clientversion", Application::ClientMinimumVersion().AsString() },
{ "name", Application::Settings.getAsString(Settings::Key::General_Name) },
{ "tags", Application::Settings.getAsString(Settings::Key::General_Tags) },
{ "guests", Application::Settings.getAsBool(Settings::Key::General_AllowGuests) ? "true" : "false" },
{ "modlist", mResourceManager.TrimmedList() },
{ "modstotalsize", std::to_string(mResourceManager.MaxModSize()) },
{ "modstotal", std::to_string(mResourceManager.ModsLoaded()) },
{ "playerslist", GetPlayers() },
{ "desc", Application::Settings.getAsString(Settings::Key::General_Description) }
};
Ret << "uuid=" << Application::Settings.Key
<< "&players=" << m_network->authenticated_client_count()
<< "&maxplayers=" << Application::Settings.MaxPlayers
<< "&port=" << Application::Settings.Port
<< "&map=" << Application::Settings.MapName
<< "&private=" << (Application::Settings.Private ? "true" : "false")
<< "&version=" << Application::ServerVersionString()
<< "&clientversion=" << std::to_string(Application::ClientMajorVersion()) + ".0" // FIXME: Wtf.
<< "&name=" << Application::Settings.ServerName
<< "&tags=" << Application::Settings.ServerTags
<< "&modlist=" << "-"//mResourceManager.TrimmedList()
<< "&modstotalsize=" << 0 //mResourceManager.MaxModSize()
<< "&modstotal=" << 0 // mResourceManager.ModsLoaded()
<< "&playerslist=" << GetPlayers()
<< "&desc=" << Application::Settings.ServerDesc
<< "&pass=" << (Application::Settings.Password.empty() ? "false" : "true");
return Ret.str();
lastCall = Ret.dump();
// Add sensitive information here because value of lastCall is used for the information packet.
Ret["uuid"] = Application::Settings.getAsString(Settings::Key::General_AuthKey);
return Ret.dump();
}
THeartbeatThread::THeartbeatThread(std::shared_ptr<Network> network)
: m_network(std::move(network)) {
THeartbeatThread::THeartbeatThread(TResourceManager& ResourceManager, TServer& Server)
: mResourceManager(ResourceManager)
, mServer(Server) {
Application::SetSubsystemStatus("Heartbeat", Application::Status::Starting);
Application::RegisterShutdownHandler([&] {
Application::SetSubsystemStatus("Heartbeat", Application::Status::ShuttingDown);
@@ -168,10 +178,15 @@ THeartbeatThread::THeartbeatThread(std::shared_ptr<Network> network)
Start();
}
std::string THeartbeatThread::GetPlayers() {
std::string players;
for (const auto& [id, client] : m_network->authenticated_clients()) {
players += client->name.get() + ";";
}
return players;
std::string Return;
mServer.ForEachClient([&](const std::weak_ptr<TClient>& ClientPtr) -> bool {
ReadLock Lock(mServer.GetClientMutex());
if (!ClientPtr.expired()) {
Return += ClientPtr.lock()->GetName() + ";";
}
return true;
});
return Return;
}
/*THeartbeatThread::~THeartbeatThread() {
}*/
+1279
View File
File diff suppressed because it is too large Load Diff
+70
View File
@@ -0,0 +1,70 @@
// BeamMP, the BeamNG.drive multiplayer mod.
// Copyright (C) 2024 BeamMP Ltd., BeamMP team and contributors.
//
// BeamMP Ltd. can be contacted by electronic mail via contact@beammp.com.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published
// by the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
#include "TLuaPlugin.h"
#include <chrono>
#include <functional>
#include <random>
#include <utility>
TLuaPlugin::TLuaPlugin(TLuaEngine& Engine, const TLuaPluginConfig& Config, const fs::path& MainFolder)
: mConfig(Config)
, mEngine(Engine)
, mFolder(MainFolder)
, mPluginName(MainFolder.stem().string())
, mFileContents(0) {
beammp_debug("Lua plugin \"" + mPluginName + "\" starting in \"" + mFolder.string() + "\"");
std::vector<fs::path> Entries;
for (const auto& Entry : fs::directory_iterator(mFolder)) {
if (Entry.is_regular_file() && Entry.path().extension() == ".lua") {
Entries.push_back(Entry);
}
}
// sort alphabetically (not needed if config is used to determine call order)
// TODO: Use config to figure out what to run in which order
std::sort(Entries.begin(), Entries.end(), [](const fs::path& first, const fs::path& second) {
auto firstStr = first.string();
auto secondStr = second.string();
std::transform(firstStr.begin(), firstStr.end(), firstStr.begin(), ::tolower);
std::transform(secondStr.begin(), secondStr.end(), secondStr.begin(), ::tolower);
return firstStr < secondStr;
});
std::vector<std::pair<fs::path, std::shared_ptr<TLuaResult>>> ResultsToCheck;
for (const auto& Entry : Entries) {
// read in entire file
try {
std::ifstream FileStream(Entry.string(), std::ios::in | std::ios::binary);
auto Size = std::filesystem::file_size(Entry);
auto Contents = std::make_shared<std::string>();
Contents->resize(Size);
FileStream.read(Contents->data(), Contents->size());
mFileContents[fs::relative(Entry).string()] = Contents;
// Execute first time
auto Result = mEngine.EnqueueScript(mConfig.StateId, TLuaChunk(Contents, Entry.string(), MainFolder.string()));
ResultsToCheck.emplace_back(Entry.string(), std::move(Result));
} catch (const std::exception& e) {
beammp_error("Error loading file \"" + Entry.string() + "\": " + e.what());
}
}
for (auto& Result : ResultsToCheck) {
Result.second->WaitUntilReady();
if (Result.second->Error) {
beammp_lua_error("Failed: \"" + Result.first.string() + "\": " + Result.second->ErrorMessage);
}
}
}
+1097
View File
File diff suppressed because it is too large Load Diff
+90
View File
@@ -0,0 +1,90 @@
// BeamMP, the BeamNG.drive multiplayer mod.
// Copyright (C) 2024 BeamMP Ltd., BeamMP team and contributors.
//
// BeamMP Ltd. can be contacted by electronic mail via contact@beammp.com.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published
// by the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
#include "TPPSMonitor.h"
#include "Client.h"
#include "TNetwork.h"
TPPSMonitor::TPPSMonitor(TServer& Server)
: mServer(Server) {
Application::SetSubsystemStatus("PPSMonitor", Application::Status::Starting);
Application::SetPPS("-");
Application::RegisterShutdownHandler([&] {
Application::SetSubsystemStatus("PPSMonitor", Application::Status::ShuttingDown);
if (mThread.joinable()) {
beammp_debug("shutting down PPSMonitor");
mThread.join();
beammp_debug("shut down PPSMonitor");
}
Application::SetSubsystemStatus("PPSMonitor", Application::Status::Shutdown);
});
Start();
}
void TPPSMonitor::operator()() {
RegisterThread("PPSMonitor");
while (!mNetwork) {
// hard(-ish) spin
std::this_thread::yield();
}
beammp_debug("PPSMonitor starting");
Application::SetSubsystemStatus("PPSMonitor", Application::Status::Good);
std::vector<std::shared_ptr<TClient>> TimedOutClients;
while (!Application::IsShuttingDown()) {
std::this_thread::sleep_for(std::chrono::seconds(1));
int C = 0, V = 0;
if (mServer.ClientCount() == 0) {
Application::SetPPS("-");
continue;
}
mServer.ForEachClient([&](const std::weak_ptr<TClient>& ClientPtr) -> bool {
std::shared_ptr<TClient> c;
{
ReadLock Lock(mServer.GetClientMutex());
if (!ClientPtr.expired()) {
c = ClientPtr.lock();
} else
return true;
}
if (c->GetCarCount() > 0) {
C++;
V += c->GetCarCount();
}
// kick on "no ping"
if (c->SecondsSinceLastPing() > (20 * 60)) {
beammp_debugf("client {} ({}) timing out: {}", c->GetID(), c->GetName(), c->SecondsSinceLastPing());
TimedOutClients.push_back(c);
} else if (c->IsSynced() && c->SecondsSinceLastPing() > (1 * 60)) {
beammp_debugf("client {} ({}) timing out: {}", c->GetName(), c->GetID(), c->SecondsSinceLastPing());
TimedOutClients.push_back(c);
}
return true;
});
for (auto& ClientToKick : TimedOutClients) {
ClientToKick->Disconnect("Timeout");
}
TimedOutClients.clear();
if (C == 0 || mInternalPPS == 0) {
Application::SetPPS("-");
} else {
int R = (mInternalPPS / C) / V;
Application::SetPPS(std::to_string(R));
}
mInternalPPS = 0;
}
}
+99
View File
@@ -0,0 +1,99 @@
// BeamMP, the BeamNG.drive multiplayer mod.
// Copyright (C) 2024 BeamMP Ltd., BeamMP team and contributors.
//
// BeamMP Ltd. can be contacted by electronic mail via contact@beammp.com.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published
// by the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
#include "TPluginMonitor.h"
#include "TLuaEngine.h"
#include <filesystem>
TPluginMonitor::TPluginMonitor(const fs::path& Path, std::shared_ptr<TLuaEngine> Engine)
: mEngine(Engine)
, mPath(Path) {
Application::SetSubsystemStatus("PluginMonitor", Application::Status::Starting);
if (!fs::exists(mPath)) {
fs::create_directories(mPath);
}
for (const auto& Entry : fs::recursive_directory_iterator(mPath, fs::directory_options::follow_directory_symlink)) {
// TODO: trigger an event when a subfolder file changes
if (Entry.is_regular_file()) {
mFileTimes[Entry.path().string()] = fs::last_write_time(Entry.path());
}
}
Application::RegisterShutdownHandler([this] {
if (mThread.joinable()) {
mThread.join();
}
});
Start();
}
void TPluginMonitor::operator()() {
RegisterThread("PluginMonitor");
beammp_info("PluginMonitor started");
Application::SetSubsystemStatus("PluginMonitor", Application::Status::Good);
while (!Application::IsShuttingDown()) {
std::vector<std::string> ToRemove;
for (const auto& Pair : mFileTimes) {
try {
auto CurrentTime = fs::last_write_time(Pair.first);
if (CurrentTime > Pair.second) {
mFileTimes[Pair.first] = CurrentTime;
// grandparent of the path should be Resources/Server
if (fs::equivalent(fs::path(Pair.first).parent_path().parent_path(), mPath)) {
if (LowerString(fs::path(Pair.first).extension().string()) == ".lua") {
beammp_infof("File \"{}\" changed, reloading", Pair.first);
// is in root folder, so reload
std::ifstream FileStream(Pair.first, std::ios::in | std::ios::binary);
auto Size = std::filesystem::file_size(Pair.first);
auto Contents = std::make_shared<std::string>();
Contents->resize(Size);
FileStream.read(Contents->data(), Contents->size());
TLuaChunk Chunk(Contents, Pair.first, fs::path(Pair.first).parent_path().string());
auto StateID = mEngine->GetStateIDForPlugin(fs::path(Pair.first).parent_path());
auto Res = mEngine->EnqueueScript(StateID, Chunk);
Res->WaitUntilReady();
if (Res->Error) {
beammp_lua_errorf("Error while hot-reloading \"{}\": {}", Pair.first, Res->ErrorMessage);
} else {
mEngine->ReportErrors(mEngine->TriggerLocalEvent(StateID, "onInit"));
mEngine->ReportErrors(mEngine->TriggerEvent("onFileChanged", "", Pair.first));
}
} else {
beammp_debugf("File \"{}\" changed, not reloading because it's not a lua file. Triggering 'onFileChanged' event instead", Pair.first);
mEngine->ReportErrors(mEngine->TriggerEvent("onFileChanged", "", Pair.first));
}
} else {
// is in subfolder, dont reload, just trigger an event
beammp_debugf("File \"{}\" changed, not reloading because it's in a subdirectory. Triggering 'onFileChanged' event instead", Pair.first);
mEngine->ReportErrors(mEngine->TriggerEvent("onFileChanged", "", Pair.first));
}
}
} catch (const std::exception& e) {
ToRemove.push_back(Pair.first);
}
}
Application::SleepSafeSeconds(3);
for (const auto& File : ToRemove) {
mFileTimes.erase(File);
beammp_warnf("File \"{}\" couldn't be accessed, so it was removed from plugin hot reload monitor (probably got deleted)", File);
}
}
Application::SetSubsystemStatus("PluginMonitor", Application::Status::Shutdown);
}
+241
View File
@@ -0,0 +1,241 @@
// BeamMP, the BeamNG.drive multiplayer mod.
// Copyright (C) 2024 BeamMP Ltd., BeamMP team and contributors.
//
// BeamMP Ltd. can be contacted by electronic mail via contact@beammp.com.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published
// by the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
#include "TResourceManager.h"
#include "Common.h"
#include <algorithm>
#include <filesystem>
#include <fmt/core.h>
#include <ios>
#include <nlohmann/json.hpp>
#include <openssl/evp.h>
namespace fs = std::filesystem;
TResourceManager::TResourceManager() {
Application::SetSubsystemStatus("ResourceManager", Application::Status::Starting);
std::string Path = Application::Settings.getAsString(Settings::Key::General_ResourceFolder) + "/Client";
if (!fs::exists(Path))
fs::create_directories(Path);
for (const auto& entry : fs::directory_iterator(Path)) {
std::string File(entry.path().string());
if (auto pos = File.find(".zip"); pos != std::string::npos) {
if (File.length() - pos == 4) {
std::replace(File.begin(), File.end(), '\\', '/');
mFileList += File + ';';
if (auto i = File.find_last_of('/'); i != std::string::npos) {
++i;
File = File.substr(i, pos - i);
}
mTrimmedList += "/" + fs::path(File).filename().string() + ';';
mFileSizes += std::to_string(size_t(fs::file_size(entry.path()))) + ';';
mMaxModSize += size_t(fs::file_size(entry.path()));
mModsLoaded++;
}
}
}
if (mModsLoaded) {
beammp_info("Loaded " + std::to_string(mModsLoaded) + " Mods");
}
Application::SetSubsystemStatus("ResourceManager", Application::Status::Good);
}
void TResourceManager::RefreshFiles() {
mMods.clear();
std::unique_lock Lock(mModsMutex);
std::string Path = Application::Settings.getAsString(Settings::Key::General_ResourceFolder) + "/Client";
nlohmann::json modsDB;
if (std::filesystem::exists(Path + "/mods.json")) {
try {
std::ifstream stream(Path + "/mods.json");
stream >> modsDB;
stream.close();
} catch (const std::exception& e) {
beammp_errorf("Failed to load mods.json: {}", e.what());
}
}
for (const auto& entry : fs::directory_iterator(Path)) {
std::string File(entry.path().string());
if (entry.path().filename().string() == "mods.json") {
continue;
}
if (entry.path().extension() != ".zip" || std::filesystem::is_directory(entry.path())) {
beammp_warnf("'{}' is not a ZIP file and will be ignored", File);
continue;
}
if (modsDB.contains(entry.path().filename().string())) {
auto& dbEntry = modsDB[entry.path().filename().string()];
if (entry.last_write_time().time_since_epoch().count() > dbEntry["lastwrite"] || std::filesystem::file_size(File) != dbEntry["filesize"].get<size_t>()) {
beammp_infof("File '{}' has been modified, rehashing", File);
} else {
dbEntry["exists"] = true;
mMods.push_back(nlohmann::json {
{ "file_name", std::filesystem::path(File).filename() },
{ "file_size", std::filesystem::file_size(File) },
{ "hash_algorithm", "sha256" },
{ "hash", dbEntry["hash"] },
{ "protected", dbEntry["protected"] } });
beammp_debugf("Mod '{}' loaded from cache", File);
continue;
}
}
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(File, std::ios::binary);
const size_t FileSize = std::filesystem::file_size(File);
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);
stream.close();
std::string result;
for (size_t i = 0; i < sha256_len; i++) {
result += fmt::format("{:02x}", sha256_value[i]);
}
beammp_debugf("sha256('{}'): {}", File, result);
mMods.push_back(nlohmann::json {
{ "file_name", std::filesystem::path(File).filename() },
{ "file_size", std::filesystem::file_size(File) },
{ "hash_algorithm", "sha256" },
{ "hash", result },
{ "protected", false } });
modsDB[std::filesystem::path(File).filename().string()] = {
{ "lastwrite", entry.last_write_time().time_since_epoch().count() },
{ "hash", result },
{ "filesize", std::filesystem::file_size(File) },
{ "protected", false },
{ "exists", true }
};
} catch (const std::exception& e) {
beammp_errorf("Sha256 hashing of '{}' failed: {}", File, e.what());
}
}
for (auto it = modsDB.begin(); it != modsDB.end();) {
if (!it.value().contains("exists")) {
it = modsDB.erase(it);
} else {
it.value().erase("exists");
++it;
}
}
try {
std::ofstream stream(Path + "/mods.json");
stream << modsDB.dump(4);
stream.close();
} catch (std::exception& e) {
beammp_error("Failed to update mod DB: " + std::string(e.what()));
}
}
void TResourceManager::SetProtected(const std::string& ModName, bool Protected) {
std::unique_lock Lock(mModsMutex);
for (auto& mod : mMods) {
if (mod["file_name"].get<std::string>() == ModName) {
mod["protected"] = Protected;
break;
}
}
auto modsDBPath = Application::Settings.getAsString(Settings::Key::General_ResourceFolder) + "/Client/mods.json";
if (std::filesystem::exists(modsDBPath)) {
try {
nlohmann::json modsDB;
std::fstream stream(modsDBPath);
stream >> modsDB;
if (modsDB.contains(ModName)) {
modsDB[ModName]["protected"] = Protected;
}
stream.clear();
stream.seekp(0, std::ios::beg);
stream << modsDB.dump(4);
stream.close();
} catch (const std::exception& e) {
beammp_errorf("Failed to update mods.json: {}", e.what());
}
}
}
+628
View File
@@ -0,0 +1,628 @@
// BeamMP, the BeamNG.drive multiplayer mod.
// Copyright (C) 2024 BeamMP Ltd., BeamMP team and contributors.
//
// BeamMP Ltd. can be contacted by electronic mail via contact@beammp.com.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published
// by the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
#include "TServer.h"
#include "Client.h"
#include "Common.h"
#include "CustomAssert.h"
#include "TLuaEngine.h"
#include "TNetwork.h"
#include "TPPSMonitor.h"
#include <TLuaPlugin.h>
#include <algorithm>
#include <any>
#include <optional>
#include <sstream>
#include <utility>
#include <nlohmann/json.hpp>
#include "LuaAPI.h"
#undef GetObject // Fixes Windows
#include "Json.h"
static std::optional<std::pair<int, int>> GetPidVid(const std::string& str) {
auto IDSep = str.find('-');
std::string pid = str.substr(0, IDSep);
std::string vid = str.substr(IDSep + 1);
if (pid.find_first_not_of("0123456789") == std::string::npos && vid.find_first_not_of("0123456789") == std::string::npos) {
try {
int PID = stoi(pid);
int VID = stoi(vid);
return { { PID, VID } };
} catch (const std::exception&) {
return std::nullopt;
}
}
return std::nullopt;
}
TEST_CASE("GetPidVid") {
SUBCASE("Valid singledigit") {
const auto MaybePidVid = GetPidVid("0-1");
CHECK(MaybePidVid);
auto [pid, vid] = MaybePidVid.value();
CHECK_EQ(pid, 0);
CHECK_EQ(vid, 1);
}
SUBCASE("Valid doubledigit") {
const auto MaybePidVid = GetPidVid("10-12");
CHECK(MaybePidVid);
auto [pid, vid] = MaybePidVid.value();
CHECK_EQ(pid, 10);
CHECK_EQ(vid, 12);
}
SUBCASE("Valid doubledigit 2") {
const auto MaybePidVid = GetPidVid("10-2");
CHECK(MaybePidVid);
auto [pid, vid] = MaybePidVid.value();
CHECK_EQ(pid, 10);
CHECK_EQ(vid, 2);
}
SUBCASE("Valid doubledigit 3") {
const auto MaybePidVid = GetPidVid("33-23");
CHECK(MaybePidVid);
auto [pid, vid] = MaybePidVid.value();
CHECK_EQ(pid, 33);
CHECK_EQ(vid, 23);
}
SUBCASE("Valid doubledigit 4") {
const auto MaybePidVid = GetPidVid("3-23");
CHECK(MaybePidVid);
auto [pid, vid] = MaybePidVid.value();
CHECK_EQ(pid, 3);
CHECK_EQ(vid, 23);
}
SUBCASE("Empty string") {
const auto MaybePidVid = GetPidVid("");
CHECK(!MaybePidVid);
}
SUBCASE("Invalid separator") {
const auto MaybePidVid = GetPidVid("0x0");
CHECK(!MaybePidVid);
}
SUBCASE("Missing pid") {
const auto MaybePidVid = GetPidVid("-0");
CHECK(!MaybePidVid);
}
SUBCASE("Missing vid") {
const auto MaybePidVid = GetPidVid("0-");
CHECK(!MaybePidVid);
}
SUBCASE("Invalid pid") {
const auto MaybePidVid = GetPidVid("x-0");
CHECK(!MaybePidVid);
}
SUBCASE("Invalid vid") {
const auto MaybePidVid = GetPidVid("0-x");
CHECK(!MaybePidVid);
}
}
TServer::TServer(const std::vector<std::string_view>& Arguments) {
beammp_info("BeamMP Server v" + Application::ServerVersionString());
Application::SetSubsystemStatus("Server", Application::Status::Starting);
Application::SetSubsystemStatus("Server", Application::Status::Good);
}
void TServer::RemoveClient(const std::weak_ptr<TClient>& WeakClientPtr) {
std::shared_ptr<TClient> LockedClientPtr { nullptr };
try {
LockedClientPtr = WeakClientPtr.lock();
} catch (const std::exception&) {
// silently fail, as there's nothing to do
return;
}
beammp_assert(LockedClientPtr != nullptr);
TClient& Client = *LockedClientPtr;
beammp_debug("removing client " + Client.GetName() + " (" + std::to_string(ClientCount()) + ")");
Client.ClearCars();
WriteLock Lock(mClientsMutex);
mClients.erase(WeakClientPtr.lock());
}
void TServer::ForEachClient(const std::function<bool(std::weak_ptr<TClient>)>& Fn) {
decltype(mClients) Clients;
{
ReadLock lock(mClientsMutex);
Clients = mClients;
}
for (auto& Client : Clients) {
if (!Fn(Client)) {
break;
}
}
}
size_t TServer::ClientCount() const {
ReadLock Lock(mClientsMutex);
return mClients.size();
}
void TServer::GlobalParser(const std::weak_ptr<TClient>& Client, std::vector<uint8_t>&& Packet, TPPSMonitor& PPSMonitor, TNetwork& Network, bool udp) {
constexpr std::string_view ABG = "ABG:";
if (Packet.size() >= ABG.size() && std::equal(Packet.begin(), Packet.begin() + ABG.size(), ABG.begin(), ABG.end())) {
Packet.erase(Packet.begin(), Packet.begin() + ABG.size());
try {
Packet = DeComp(Packet);
} catch (const InvalidDataError& ) {
auto LockedClient = Client.lock();
beammp_errorf("Failed to decompress packet from client {}. The client sent invalid data and will now be disconnected.", LockedClient->GetID());
Network.ClientKick(*LockedClient, "Sent invalid compressed packet (this is likely a bug on your end)");
return;
} catch (const std::runtime_error& e) {
auto LockedClient = Client.lock();
beammp_errorf("Failed to decompress packet from client {}: {}. The server might be out of RAM! The client will now be disconnected.", LockedClient->GetID(), e.what());
Network.ClientKick(*LockedClient, "Decompression failed (likely a server-side problem)");
return;
}
}
if (Packet.empty()) {
return;
}
if (Client.expired()) {
return;
}
auto LockedClient = Client.lock();
std::any Res;
char Code = Packet.at(0);
std::string StringPacket(reinterpret_cast<const char*>(Packet.data()), Packet.size());
// V to Y
if (Code <= 89 && Code >= 86) {
int PID = -1;
int VID = -1;
auto pidVidPart = StringPacket.substr(3);
auto MaybePidVid = GetPidVid(pidVidPart.substr(0, pidVidPart.find(':')));
if (MaybePidVid) {
std::tie(PID, VID) = MaybePidVid.value();
}
if (PID == -1 || VID == -1 || PID != LockedClient->GetID()) {
return;
}
PPSMonitor.IncrementInternalPPS();
Network.SendToAll(LockedClient.get(), Packet, false, false);
return;
}
switch (Code) {
case 'H': // initial connection
if (udp) {
beammp_debugf("Received 'H' packet over UDP from client '{}' ({}), ignoring it", LockedClient->GetName(), LockedClient->GetID());
return;
}
if (!Network.SyncClient(Client)) {
// TODO handle
}
return;
case 'p':
if (!Network.Respond(*LockedClient, StringToVector("p"), false)) {
// failed to send
LockedClient->Disconnect("Failed to send ping");
} else {
Network.UpdatePlayer(*LockedClient);
}
return;
case 'O':
if (udp) {
beammp_debugf("Received 'O' packet over UDP from client '{}' ({}), ignoring it", LockedClient->GetName(), LockedClient->GetID());
return;
}
if (Packet.size() > 1000) {
beammp_debug(("Received data from: ") + LockedClient->GetName() + (" Size: ") + std::to_string(Packet.size()));
}
ParseVehicle(*LockedClient, StringPacket, Network);
return;
case 'C': {
if (udp) {
beammp_debugf("Received 'C' packet over UDP from client '{}' ({}), ignoring it", LockedClient->GetName(), LockedClient->GetID());
return;
}
if (Packet.size() < 4 || std::find(Packet.begin() + 3, Packet.end(), ':') == Packet.end())
break;
const auto PacketAsString = std::string(reinterpret_cast<const char*>(Packet.data()), Packet.size());
std::string Message = "";
const auto ColonPos = PacketAsString.find(':', 3);
if (ColonPos != std::string::npos && ColonPos + 2 < PacketAsString.size()) {
Message = PacketAsString.substr(ColonPos + 2);
}
if (Message.empty()) {
beammp_debugf("Empty chat message received from '{}' ({}), ignoring it", LockedClient->GetName(), LockedClient->GetID());
return;
}
if (Message.size() > 500) {
beammp_debugf("Chat message too long from '{}' ({}), ignoring it", LockedClient->GetName(), LockedClient->GetID());
return;
}
auto Futures = LuaAPI::MP::Engine->TriggerEvent("onChatMessage", "", LockedClient->GetID(), LockedClient->GetName(), Message);
TLuaEngine::WaitForAll(Futures);
LogChatMessage(LockedClient->GetName(), LockedClient->GetID(), PacketAsString.substr(PacketAsString.find(':', 3) + 1));
bool Rejected = std::any_of(Futures.begin(), Futures.end(),
[](const std::shared_ptr<TLuaResult>& Elem) {
return !Elem->Error
&& Elem->Result.is<int>()
&& bool(Elem->Result.as<int>());
});
if (!Rejected) {
std::string SanitizedPacket = fmt::format("C:{}: {}", LockedClient->GetName(), Message);
Network.SendToAll(nullptr, StringToVector(SanitizedPacket), true, true);
}
auto PostFutures = LuaAPI::MP::Engine->TriggerEvent("postChatMessage", "", !Rejected, LockedClient->GetID(), LockedClient->GetName(), Message);
LuaAPI::MP::Engine->ReportErrors(PostFutures);
return;
}
case 'E':
if (udp) {
beammp_debugf("Received 'E' packet over UDP from client '{}' ({}), ignoring it", LockedClient->GetName(), LockedClient->GetID());
return;
}
HandleEvent(*LockedClient, StringPacket);
return;
case 'N':
Network.SendToAll(LockedClient.get(), Packet, false, true);
return;
case 'Z': { // position packet
PPSMonitor.IncrementInternalPPS();
int PID = -1;
int VID = -1;
auto pidVidPart = StringPacket.substr(3);
auto MaybePidVid = GetPidVid(pidVidPart.substr(0, pidVidPart.find(':')));
if (MaybePidVid) {
std::tie(PID, VID) = MaybePidVid.value();
}
if (PID == -1 || VID == -1 || PID != LockedClient->GetID()) {
return;
}
Network.SendToAll(LockedClient.get(), Packet, false, false);
HandlePosition(*LockedClient, StringPacket);
return;
}
default:
return;
}
}
void TServer::HandleEvent(TClient& c, const std::string& RawData) {
// E:Name:Data
// Data is allowed to have ':'
if (RawData.size() < 2) {
beammp_debugf("Client '{}' ({}) tried to send an empty event, ignoring", c.GetName(), c.GetID());
return;
}
auto NameDataSep = RawData.find(':', 2);
if (NameDataSep == std::string::npos) {
beammp_warn("received event in invalid format (missing ':'), got: '" + RawData + "'");
}
std::string Name = RawData.substr(2, NameDataSep - 2);
std::string Data = RawData.substr(NameDataSep + 1);
std::vector<std::string> exclude = {"onInit", "onFileChanged","onVehicleDeleted","onConsoleInput","onPlayerAuth","postPlayerAuth", "onPlayerDisconnect",
"onPlayerConnecting","onPlayerJoining","onPlayerJoin","onChatMessage","postChatMessage","onVehicleSpawn","postVehicleSpawn","onVehicleEdited", "postVehicleEdited",
"onVehicleReset","onVehiclePaintChanged","onShutdown"};
if (std::ranges::find(exclude, Name) != exclude.end()) {
beammp_debugf("Excluded event triggered by client '{}' ({}): '{}', ignoring.", c.GetName(), c.GetID(), Name);
return;
}
LuaAPI::MP::Engine->ReportErrors(LuaAPI::MP::Engine->TriggerEvent(Name, "", c.GetID(), Data));
}
bool TServer::IsUnicycle(TClient& c, const std::string& CarJson) {
try {
auto Car = nlohmann::json::parse(CarJson);
const std::string jbm = "jbm";
if (Car.contains(jbm) && Car[jbm].is_string() && Car[jbm] == "unicycle") {
return true;
}
} catch (const std::exception& e) {
beammp_warn("Failed to parse vehicle data as json for client " + std::to_string(c.GetID()) + ": '" + CarJson + "'.");
}
return false;
}
bool TServer::ShouldSpawn(TClient& c, const std::string& CarJson, int ID) {
if (IsUnicycle(c, CarJson) && c.GetUnicycleID() < 0) {
c.SetUnicycleID(ID);
return true;
} else {
return c.GetCarCount() < Application::Settings.getAsInt(Settings::Key::General_MaxCars);
}
}
void TServer::ParseVehicle(TClient& c, const std::string& Pckt, TNetwork& Network) {
if (Pckt.length() < 6)
return;
std::string Packet = Pckt;
char Code = Packet.at(1);
int PID = -1;
int VID = -1;
std::string Data = Packet.substr(3), pid, vid;
switch (Code) { // Spawned Destroyed Switched/Moved NotFound Reset
case 's':
beammp_tracef("got 'Os' packet: '{}' ({})", Packet, Packet.size());
if (Data.at(0) == '0') {
int CarID = c.GetOpenCarID();
beammp_debugf("'{}' created a car with ID {}", c.GetName(), CarID);
std::string CarJson = Packet.substr(5);
Packet = "Os:" + c.GetRoles() + ":" + c.GetName() + ":" + std::to_string(c.GetID()) + "-" + std::to_string(CarID) + ":" + CarJson;
auto Futures = LuaAPI::MP::Engine->TriggerEvent("onVehicleSpawn", "", c.GetID(), CarID, Packet.substr(3));
TLuaEngine::WaitForAll(Futures);
bool ShouldntSpawn = std::any_of(Futures.begin(), Futures.end(),
[](const std::shared_ptr<TLuaResult>& Result) {
return !Result->Error && Result->Result.is<int>() && Result->Result.as<int>() != 0;
});
bool SpawnConfirmed = false;
auto CarJsonDoc = nlohmann::json::parse(CarJson, nullptr, false);
if (ShouldSpawn(c, CarJson, CarID) && !ShouldntSpawn && !CarJsonDoc.is_discarded()) {
c.AddNewCar(CarID, CarJsonDoc);
Network.SendToAll(nullptr, StringToVector(Packet), true, true);
SpawnConfirmed = true;
} else {
if (!Network.Respond(c, StringToVector(Packet), true)) {
// TODO: handle
}
std::string Destroy = "Od:" + std::to_string(c.GetID()) + "-" + std::to_string(CarID);
LuaAPI::MP::Engine->ReportErrors(LuaAPI::MP::Engine->TriggerEvent("onVehicleDeleted", "", c.GetID(), CarID));
if (!Network.Respond(c, StringToVector(Destroy), true)) {
// TODO: handle
}
beammp_debugf("{} (force : car limit/lua) removed ID {}", c.GetName(), CarID);
SpawnConfirmed = false;
}
auto PostFutures = LuaAPI::MP::Engine->TriggerEvent("postVehicleSpawn", "", SpawnConfirmed, c.GetID(), CarID, Packet.substr(3));
// the post event is not cancellable so we dont wait for it
LuaAPI::MP::Engine->ReportErrors(PostFutures);
}
return;
case 'c': {
beammp_trace(std::string(("got 'Oc' packet: '")) + Packet + ("' (") + std::to_string(Packet.size()) + (")"));
auto MaybePidVid = GetPidVid(Data.substr(0, Data.find(':', 1)));
if (MaybePidVid) {
std::tie(PID, VID) = MaybePidVid.value();
}
if (PID != -1 && VID != -1 && PID == c.GetID()) {
auto Futures = LuaAPI::MP::Engine->TriggerEvent("onVehicleEdited", "", c.GetID(), VID, Packet.substr(3));
TLuaEngine::WaitForAll(Futures);
bool ShouldntAllow = std::any_of(Futures.begin(), Futures.end(),
[](const std::shared_ptr<TLuaResult>& Result) {
return !Result->Error && Result->Result.is<int>() && Result->Result.as<int>() != 0;
});
auto FoundPos = Packet.find('{');
FoundPos = FoundPos == std::string::npos ? 0 : FoundPos; // attempt at sanitizing this
bool Allowed = false;
if ((c.GetUnicycleID() != VID || IsUnicycle(c, Packet.substr(FoundPos)))
&& !ShouldntAllow) {
Network.SendToAll(&c, StringToVector(Packet), false, true);
Apply(c, VID, Packet);
Allowed = true;
} else {
if (c.GetUnicycleID() == VID) {
c.SetUnicycleID(-1);
}
std::string Destroy = "Od:" + std::to_string(c.GetID()) + "-" + std::to_string(VID);
Network.SendToAll(nullptr, StringToVector(Destroy), true, true);
LuaAPI::MP::Engine->ReportErrors(LuaAPI::MP::Engine->TriggerEvent("onVehicleDeleted", "", c.GetID(), VID));
c.DeleteCar(VID);
Allowed = false;
}
auto PostFutures = LuaAPI::MP::Engine->TriggerEvent("postVehicleEdited", "", Allowed, c.GetID(), VID, Packet.substr(3));
// the post event is not cancellable so we dont wait for it
LuaAPI::MP::Engine->ReportErrors(PostFutures);
}
return;
}
case 'd': {
beammp_trace(std::string(("got 'Od' packet: '")) + Packet + ("' (") + std::to_string(Packet.size()) + (")"));
auto MaybePidVid = GetPidVid(Data.substr(0, Data.find(':', 1)));
if (MaybePidVid) {
std::tie(PID, VID) = MaybePidVid.value();
}
if (PID != -1 && VID != -1 && PID == c.GetID()) {
if (c.GetUnicycleID() == VID) {
c.SetUnicycleID(-1);
}
Network.SendToAll(nullptr, StringToVector(Packet), true, true);
// TODO: should this trigger on all vehicle deletions?
LuaAPI::MP::Engine->ReportErrors(LuaAPI::MP::Engine->TriggerEvent("onVehicleDeleted", "", c.GetID(), VID));
c.DeleteCar(VID);
beammp_debug(c.GetName() + (" deleted car with ID ") + std::to_string(VID));
}
return;
}
case 'r': {
beammp_trace(std::string(("got 'Or' packet: '")) + Packet + ("' (") + std::to_string(Packet.size()) + (")"));
auto MaybePidVid = GetPidVid(Data.substr(0, Data.find(':', 1)));
if (MaybePidVid) {
std::tie(PID, VID) = MaybePidVid.value();
}
if (PID != -1 && VID != -1 && PID == c.GetID()) {
auto BracketPos = Data.find('{');
if (BracketPos == std::string::npos) {
beammp_debugf("Invalid 'Or' packet body from client {}", c.GetID());
return;
}
Data = Data.substr(BracketPos);
LuaAPI::MP::Engine->ReportErrors(LuaAPI::MP::Engine->TriggerEvent("onVehicleReset", "", c.GetID(), VID, Data));
Network.SendToAll(&c, StringToVector(Packet), false, true);
}
return;
}
case 't': {
beammp_trace(std::string(("got 'Ot' packet: '")) + Packet + ("' (") + std::to_string(Packet.size()) + (")"));
auto MaybePidVid = GetPidVid(Data.substr(0, Data.find(':', 1)));
if (MaybePidVid) {
std::tie(PID, VID) = MaybePidVid.value();
}
if (PID != -1 && VID != -1 && PID == c.GetID()) {
Network.SendToAll(&c, StringToVector(Packet), false, true);
}
return;
}
case 'm': {
Network.SendToAll(&c, StringToVector(Packet), false, true);
return;
}
case 'p': {
beammp_trace(std::string(("got 'Op' packet: '")) + Packet + ("' (") + std::to_string(Packet.size()) + (")"));
auto MaybePidVid = GetPidVid(Data.substr(0, Data.find(':', 1)));
if (MaybePidVid) {
std::tie(PID, VID) = MaybePidVid.value();
}
if (PID != -1 && VID != -1 && PID == c.GetID()) {
auto BracketPos = Data.find('[');
if (BracketPos == std::string::npos) {
beammp_debugf("Invalid 'Op' packet body from client {}", c.GetID());
return;
}
Data = Data.substr(BracketPos);
LuaAPI::MP::Engine->ReportErrors(LuaAPI::MP::Engine->TriggerEvent("onVehiclePaintChanged", "", c.GetID(), VID, Data));
Network.SendToAll(&c, StringToVector(Packet), false, true);
auto CarData = c.GetCarData(VID);
if (CarData == nlohmann::detail::value_t::null)
return;
if (CarData.contains("vcf") && CarData.at("vcf").is_object())
if (CarData.at("vcf").contains("paints") && CarData.at("vcf").at("paints").is_array()) {
CarData.at("vcf")["paints"] = nlohmann::json::parse(Data);
c.SetCarData(VID, CarData);
}
}
return;
}
default:
beammp_trace(std::string(("possibly not implemented: '") + Packet + ("' (") + std::to_string(Packet.size()) + (")")));
return;
}
}
void TServer::Apply(TClient& c, int VID, const std::string& pckt) {
auto FoundPos = pckt.find('{');
if (FoundPos == std::string::npos) {
beammp_error("Malformed packet received, no '{' found");
return;
}
std::string Packet = pckt.substr(FoundPos);
nlohmann::json VD = c.GetCarData(VID);
if (VD == nlohmann::detail::value_t::null) {
beammp_error("Tried to apply change to vehicle that does not exist");
return;
}
nlohmann::json Pack = nlohmann::json::parse(Packet, nullptr, false);
if (Pack.is_discarded()) {
beammp_error("Could not get active vehicle config!");
return;
}
c.SetCarData(VID, Pack);
}
void TServer::InsertClient(const std::shared_ptr<TClient>& NewClient) {
beammp_debug("inserting client (" + std::to_string(ClientCount()) + ")");
WriteLock Lock(mClientsMutex); // TODO why is there 30+ threads locked here
(void)mClients.insert(NewClient);
}
struct PidVidData {
int PID;
int VID;
std::string Data;
};
static std::optional<PidVidData> ParsePositionPacket(const std::string& Packet) {
if (Packet.size() < 3) {
// invalid packet
return std::nullopt;
}
// Zp:PID-VID:DATA
std::string withoutCode = Packet.substr(3);
// parse veh ID
if (auto DataBeginPos = withoutCode.find('{'); DataBeginPos != std::string::npos && DataBeginPos != 0) {
// separator is :{, so position of { minus one
auto PidVidOnly = withoutCode.substr(0, DataBeginPos - 1);
auto MaybePidVid = GetPidVid(PidVidOnly);
if (MaybePidVid) {
int PID = -1;
int VID = -1;
// FIXME: check that the VID and PID are valid, so that we don't waste memory
std::tie(PID, VID) = MaybePidVid.value();
std::string Data = withoutCode.substr(DataBeginPos);
return PidVidData {
.PID = PID,
.VID = VID,
.Data = Data,
};
} else {
// invalid packet
return std::nullopt;
}
}
// invalid packet
return std::nullopt;
}
TEST_CASE("ParsePositionPacket") {
const auto TestData = R"({"tim":10.428000331623,"vel":[-2.4171722121385e-05,-9.7184734153252e-06,-7.6420763232237e-06],"rot":[-0.0001296154171915,0.0031575385950029,0.98994906610295,0.14138903660382],"rvel":[5.3640324636461e-05,-9.9824529946024e-05,5.1664064641372e-05],"pos":[-0.27281248907838,-0.20515357944633,0.49695488960431],"ping":0.032999999821186})";
SUBCASE("All the pids and vids") {
for (int pid = 0; pid < 100; ++pid) {
for (int vid = 0; vid < 100; ++vid) {
std::optional<PidVidData> MaybeRes = ParsePositionPacket(fmt::format("Zp:{}-{}:{}", pid, vid, TestData));
CHECK(MaybeRes.has_value());
CHECK_EQ(MaybeRes.value().PID, pid);
CHECK_EQ(MaybeRes.value().VID, vid);
CHECK_EQ(MaybeRes.value().Data, TestData);
}
}
}
}
void TServer::HandlePosition(TClient& c, const std::string& Packet) {
if (auto Parsed = ParsePositionPacket(Packet); Parsed.has_value()) {
c.SetCarPosition(Parsed.value().VID, Parsed.value().Data);
}
}
-478
View File
@@ -1,478 +0,0 @@
#include "Value.h"
#include "Common.h"
#include "boost/json/value_from.hpp"
#include "sol/as_args.hpp"
#include "sol/types.hpp"
#include <doctest/doctest.h>
#include <fmt/format.h>
#include <sol/sol.hpp>
ValueToStringVisitor::ValueToStringVisitor(Flag flags, int depth)
: m_quote_strings(flags & QUOTE_STRINGS)
, m_depth(depth) {
}
std::string ValueToStringVisitor::operator()(const std::string& str) const {
if (m_quote_strings) {
return fmt::format("\"{}\"", str);
} else {
return str;
}
}
std::string ValueToStringVisitor::operator()(int64_t i) const {
return fmt::format("{}", i);
}
std::string ValueToStringVisitor::operator()(double d) const {
return fmt::format("{}", d);
}
std::string ValueToStringVisitor::operator()(Null) const {
return "null";
}
std::string ValueToStringVisitor::operator()(Bool b) const {
return b ? "true" : "false";
}
std::string ValueToStringVisitor::operator()(const ValueArray& array) const {
std::string res = "[ ";
size_t i = 0;
for (const auto& elem : array) {
res += fmt::format("\n{:>{}}{}", "", m_depth * 2, boost::apply_visitor(ValueToStringVisitor(QUOTE_STRINGS, m_depth + 1), elem));
if (i + 2 <= array.size()) {
res += ",";
} else {
res += "\n";
}
++i;
}
return res += fmt::format("{:>{}}]", "", (m_depth == 0 ? 0 : (m_depth - 1) * 2));
}
std::string ValueToStringVisitor::operator()(const ValueTuple& array) const {
std::string res = "( ";
size_t i = 0;
for (const auto& elem : array) {
res += fmt::format("\n{:>{}}{}", "", m_depth * 2, boost::apply_visitor(ValueToStringVisitor(QUOTE_STRINGS, m_depth + 1), elem));
if (i + 2 <= array.size()) {
res += ",";
} else {
res += "\n";
}
++i;
}
return res += fmt::format("{:>{}})", "", (m_depth == 0 ? 0 : (m_depth - 1) * 2));
}
std::string ValueToStringVisitor::operator()(const HashMap<std::string, Value>& map) const {
std::string res = "{ ";
size_t i = 0;
for (const auto& [key, value] : map) {
res += fmt::format("\n{:>{}}{}: {}", "", m_depth * 2, key, boost::apply_visitor(ValueToStringVisitor(QUOTE_STRINGS, m_depth + 1), value));
if (i + 2 <= map.size()) {
res += ",";
} else {
res += "\n";
}
++i;
}
return res += fmt::format("{:>{}}}}", "", (m_depth == 0 ? 0 : (m_depth - 1) * 2));
}
TEST_CASE("Value constructors") {
SUBCASE("string via const char*") {
Value value = "hello, world";
CHECK_EQ(value.which(), VALUE_TYPE_IDX_STRING);
}
SUBCASE("int via long literal") {
Value value = int64_t(1l);
CHECK_EQ(value.which(), VALUE_TYPE_IDX_INT);
}
SUBCASE("double via double literal") {
Value value = 5.432;
CHECK_EQ(value.which(), VALUE_TYPE_IDX_DOUBLE);
}
// other constructors must be explicit as far as we're concerned
}
TEST_CASE("ValueToStringVisitor") {
SUBCASE("string quoted") {
Value value = "hello, world";
// expected to implicitly be "ValueToStringVisitor::Flag::QUOTE_STRINGS"
std::string res = boost::apply_visitor(ValueToStringVisitor(), value);
CHECK_EQ(res, "\"hello, world\"");
}
SUBCASE("string not quoted") {
Value value = "hello, world";
std::string res = boost::apply_visitor(ValueToStringVisitor(ValueToStringVisitor::Flag::NONE), value);
CHECK_EQ(res, "hello, world");
}
SUBCASE("int") {
Value value = int64_t(123456789l);
std::string res = boost::apply_visitor(ValueToStringVisitor(), value);
CHECK_EQ(res, "123456789");
}
SUBCASE("negative int") {
Value value = int64_t(-123456789l);
std::string res = boost::apply_visitor(ValueToStringVisitor(), value);
CHECK_EQ(res, "-123456789");
}
SUBCASE("whole integer double") {
Value value = 123456789.0;
std::string res = boost::apply_visitor(ValueToStringVisitor(), value);
CHECK_EQ(res, "123456789");
}
SUBCASE("double") {
Value value = 1234.56789;
std::string res = boost::apply_visitor(ValueToStringVisitor(), value);
CHECK_EQ(res, "1234.56789");
}
SUBCASE("null") {
Value value = Null {};
std::string res = boost::apply_visitor(ValueToStringVisitor(), value);
CHECK_EQ(res, "null");
} /*
SUBCASE("array") {
Value value = ValueArray { 1l, 2l, "hello", 5.4 };
std::string res = boost::apply_visitor(ValueToStringVisitor(), value);
CHECK_EQ(res, "[ 1, 2, \"hello\", 5.4 ]");
}
SUBCASE("tuple") {
Value value = ValueArray { 1l, 2l, "hello" };
std::string res = boost::apply_visitor(ValueToStringVisitor(), value);
CHECK_EQ(res, "( 1, 2, \"hello\" )");
}
SUBCASE("array with array inside") {
Value value = ValueArray { 1l, 2l, "hello", ValueArray { -1l, -2l }, 5.4 };
std::string res = boost::apply_visitor(ValueToStringVisitor(), value);
CHECK_EQ(res, "[ 1, 2, \"hello\", [ -1, -2 ], 5.4 ]");
}
SUBCASE("map") {
Value value = ValueHashMap { { "hello", "world" }, { "x", 53.5 } };
std::string res = boost::apply_visitor(ValueToStringVisitor(), value);
CHECK_EQ(res, R"({ hello="world", x=53.5 })");
}
SUBCASE("map with map inside") {
Value value = ValueHashMap { { "hello", "world" }, { "my map", ValueHashMap { { "value", 1l } } }, { "x", 53.5 } };
std::string res = boost::apply_visitor(ValueToStringVisitor(), value);
CHECK_EQ(res, R"({ hello="world", my map={ value=1 }, x=53.5 })");
}
*/
SUBCASE("empty array") {
Value value = ValueArray {};
std::string res = boost::apply_visitor(ValueToStringVisitor(), value);
CHECK_EQ(res, "[ ]");
}
SUBCASE("empty tuple") {
Value value = ValueTuple {};
std::string res = boost::apply_visitor(ValueToStringVisitor(), value);
CHECK_EQ(res, "( )");
}
SUBCASE("empty map") {
Value value = ValueHashMap {};
std::string res = boost::apply_visitor(ValueToStringVisitor(), value);
CHECK_EQ(res, "{ }");
}
SUBCASE("bool") {
Value value = Bool { false };
std::string res = boost::apply_visitor(ValueToStringVisitor(), value);
CHECK_EQ(res, "false");
value = Bool { true };
res = boost::apply_visitor(ValueToStringVisitor(), value);
CHECK_EQ(res, "true");
}
}
static Result<Value> sol_obj_to_value_impl(const sol::object& obj, const std::function<Result<Value>(const sol::object&)>& invalid_provider, size_t max_depth, size_t depth) {
if (depth == max_depth) {
return { Null {} };
}
++depth;
switch (obj.get_type()) {
case sol::type::none:
case sol::type::lua_nil:
return { Null {} };
case sol::type::string:
return { obj.as<std::string>() };
case sol::type::number:
if (obj.is<int64_t>()) {
return { obj.as<int64_t>() };
} else {
return { obj.as<double>() };
}
case sol::type::thread: {
if (invalid_provider) {
return invalid_provider(obj);
} else {
return Result<Value>("Can't convert {} to Value", "lua thread");
}
}
case sol::type::boolean:
return { Bool { obj.as<bool>() } };
case sol::type::function:
if (invalid_provider) {
return invalid_provider(obj);
} else {
return Result<Value>("Can't convert {} to Value", "lua function");
}
case sol::type::userdata:
if (invalid_provider) {
return invalid_provider(obj);
} else {
return Result<Value>("Can't convert {} to Value", "lua userdata");
}
case sol::type::lightuserdata:
if (invalid_provider) {
return invalid_provider(obj);
} else {
return Result<Value>("Can't convert {} to Value", "lua lightuserdata");
}
case sol::type::table: {
bool is_map = false;
// look through all keys, if all are numbers, its not a map
for (const auto& [k, v] : obj.as<sol::table>()) {
// due to a quirk in lua+sol (sol issue #247), you have to
// both check that it's a number, but also that its an integer number.
// technically, lua allows float keys in an array (i guess?)
// but for us that counts as a type we need to use a hashmap for.
// k.is<double>() would be true for any number type, but
// k.is<int>() is only true for such numbers that are non-float.
if (k.get_type() == sol::type::number) {
if (!k.is<int64_t>()) {
is_map = true;
break;
}
} else if (k.get_type() == sol::type::string) {
is_map = true;
break;
} else {
return Result<Value>("Can't use non-string and non-number object as key for table (type id is {})", int(k.get_type())); // TODO: make a fucntion to fix htis messy way to enfore sending an error
}
}
if (is_map) {
ValueHashMap map;
for (const auto& [k, v] : obj.as<sol::table>()) {
std::string key;
if (k.get_type() == sol::type::number) {
if (k.is<int64_t>()) {
key = fmt::format("{}", k.as<int64_t>());
} else {
key = fmt::format("{:F}", k.as<double>());
}
} else if (k.get_type() == sol::type::string) {
key = k.as<std::string>();
} else {
return Result<Value>("Failed to construct hash-map: Can't use non-string and non-number object as key for table{}", "");
}
auto maybe_val = sol_obj_to_value_impl(v, invalid_provider, max_depth, depth);
if (maybe_val) [[likely]] {
map.emplace(key, maybe_val.move());
} else {
return maybe_val; // error
}
}
return { std::move(map) };
} else {
ValueArray array;
for (const auto& [k, v] : obj.as<sol::table>()) {
auto i = k.as<int64_t>() - 1; // -1 due to lua arrays starting at 1
if (size_t(i) >= array.size()) {
array.resize(size_t(i) + 1, Null {});
}
auto maybe_val = sol_obj_to_value_impl(v, invalid_provider, max_depth, depth);
if (maybe_val) [[likely]] {
array[size_t(i)] = maybe_val.move();
} else {
return maybe_val; // error
}
}
return { std::move(array) };
}
}
case sol::type::poly:
if (invalid_provider) {
return invalid_provider(obj);
} else {
return Result<Value>("Can't convert {} to Value", "lua poly");
}
default:
break;
}
return Result<Value>("Unknown type, can't convert to value.{}", "");
}
Result<Value> sol_obj_to_value(const sol::object& obj, const std::function<Result<Value>(const sol::object&)>& invalid_provider, size_t max_depth) {
return sol_obj_to_value_impl(obj, invalid_provider, max_depth, 0);
}
TEST_CASE("sol_obj_to_value") {
sol::state state {};
SUBCASE("nil") {
sol::table obj = state.create_table();
sol::object o = obj.get<sol::object>(0);
CHECK_EQ(sol_obj_to_value(o).value().which(), VALUE_TYPE_IDX_NULL);
}
SUBCASE("string") {
auto res = sol::make_object(state.lua_state(), "hello");
CHECK_EQ(sol_obj_to_value(res).value().which(), VALUE_TYPE_IDX_STRING);
auto val = sol_obj_to_value(res).value();
auto str = boost::get<std::string>(val);
CHECK_EQ(str, "hello");
}
SUBCASE("int") {
auto res = sol::make_object(state.lua_state(), 1234l);
CHECK_EQ(sol_obj_to_value(res).value().which(), VALUE_TYPE_IDX_INT);
auto val = sol_obj_to_value(res).value();
auto i = boost::get<int64_t>(val);
CHECK_EQ(i, 1234l);
}
SUBCASE("double") {
auto res = sol::make_object(state.lua_state(), 53.3);
CHECK_EQ(sol_obj_to_value(res).value().which(), VALUE_TYPE_IDX_DOUBLE);
auto val = sol_obj_to_value(res).value();
auto d = boost::get<double>(val);
CHECK_EQ(d, 53.3);
}
SUBCASE("bool") {
auto res = sol::make_object(state.lua_state(), true);
CHECK_EQ(sol_obj_to_value(res).value().which(), VALUE_TYPE_IDX_BOOL);
auto val = sol_obj_to_value(res).value();
auto d = boost::get<Bool>(val);
CHECK_EQ(bool(d), true);
}
SUBCASE("table (map)") {
auto res = state.create_table_with(
"hello", "world",
"x", 35l);
CHECK_EQ(sol_obj_to_value(res).value().which(), VALUE_TYPE_IDX_HASHMAP);
auto val = sol_obj_to_value(res).value();
auto m = boost::get<ValueHashMap>(val);
CHECK(m.contains("hello"));
CHECK(m.contains("x"));
CHECK_EQ(boost::get<std::string>(m["hello"]), "world");
CHECK_EQ(boost::get<int64_t>(m["x"]), 35l);
}
SUBCASE("table (array)") {
auto res = state.create_table_with(
1, 1,
2, 2,
3, 3,
6, 6);
CHECK_EQ(sol_obj_to_value(res).value().which(), VALUE_TYPE_IDX_ARRAY);
auto val = sol_obj_to_value(res).value();
auto m = boost::get<ValueArray>(val);
CHECK_EQ(boost::get<int64_t>(m[0]), 1);
CHECK_EQ(boost::get<int64_t>(m[1]), 2);
CHECK_EQ(boost::get<int64_t>(m[2]), 3);
CHECK_EQ(m[3].which(), VALUE_TYPE_IDX_NULL);
CHECK_EQ(m[4].which(), VALUE_TYPE_IDX_NULL);
CHECK_EQ(boost::get<int64_t>(m[5]), 6);
}
// TODO: add test for all the invalid types
}
std::ostream& operator<<(std::ostream& os, const Null&) {
return os << "null";
}
std::ostream& operator<<(std::ostream& os, const Bool& b) {
return os << (b ? "true" : "false");
}
boost::json::value ValueToJsonVisitor::operator()(const std::string& str) const {
return boost::json::value_from(str);
}
boost::json::value ValueToJsonVisitor::operator()(int64_t i) const {
return boost::json::value_from(i);
}
boost::json::value ValueToJsonVisitor::operator()(double d) const {
return boost::json::value_from(d);
}
boost::json::value ValueToJsonVisitor::operator()(Null) const {
return boost::json::value_from(nullptr);
}
boost::json::value ValueToJsonVisitor::operator()(Bool b) const {
return boost::json::value_from(b.b);
}
boost::json::value ValueToJsonVisitor::operator()(const ValueArray& array) const {
auto ja = boost::json::array();
for (const auto& val : array) {
auto obj = boost::apply_visitor(ValueToJsonVisitor(), val);
ja.push_back(obj);
}
return ja;
}
boost::json::value ValueToJsonVisitor::operator()(const ValueTuple& tuple) const {
auto ja = boost::json::array();
for (const auto& val : tuple) {
auto obj = boost::apply_visitor(ValueToJsonVisitor(), val);
ja.push_back(obj);
}
return ja;
}
boost::json::value ValueToJsonVisitor::operator()(const HashMap<std::string, Value>& map) const {
auto jo = boost::json::object();
for (const auto& [key, val] : map) {
auto json_val = boost::apply_visitor(ValueToJsonVisitor(), val);
jo.emplace(std::string(key), json_val);
}
return jo;
}
ValueToLuaVisitor::ValueToLuaVisitor(sol::state& state)
: m_state(state) {
}
sol::object ValueToLuaVisitor::operator()(const std::string& str) const {
return sol::make_object(m_state.lua_state(), str);
}
sol::object ValueToLuaVisitor::operator()(int64_t i) const {
return sol::make_object(m_state.lua_state(), i);
}
sol::object ValueToLuaVisitor::operator()(double d) const {
return sol::make_object(m_state.lua_state(), d);
}
sol::object ValueToLuaVisitor::operator()(Null) const {
return sol::lua_nil_t();
}
sol::object ValueToLuaVisitor::operator()(Bool b) const {
return sol::make_object(m_state.lua_state(), b.b);
}
sol::object ValueToLuaVisitor::operator()(const ValueArray& array) const {
auto table = m_state.create_table();
for (const auto& val : array) {
table.add(boost::apply_visitor(*this, val));
}
return table;
}
sol::object ValueToLuaVisitor::operator()(const ValueTuple& tuple) const {
auto table = m_state.create_table();
for (const auto& val : tuple) {
table.add(boost::apply_visitor(*this, val));
}
return table;
}
sol::object ValueToLuaVisitor::operator()(const HashMap<std::string, Value>& map) const {
auto table = m_state.create_table();
for (const auto& [key, val] : map) {
table.set(key, boost::apply_visitor(*this, val));
}
return table;
}
+5 -1
View File
@@ -21,7 +21,7 @@
#include "Common.h"
#include <utility>
TVehicleData::TVehicleData(int ID, std::string Data)
TVehicleData::TVehicleData(int ID, nlohmann::json Data)
: mID(ID)
, mData(std::move(Data)) {
beammp_trace("vehicle " + std::to_string(mID) + " constructed");
@@ -30,3 +30,7 @@ TVehicleData::TVehicleData(int ID, std::string Data)
TVehicleData::~TVehicleData() {
beammp_trace("vehicle " + std::to_string(mID) + " destroyed");
}
std::string TVehicleData::DataAsPacket(const std::string& Role, const std::string& Name, const int ID) const {
return "Os:" + Role + ":" + Name + ":" + std::to_string(ID) + "-" + std::to_string(this->mID) + ":" + this->mData.dump();
}
+59 -37
View File
@@ -20,27 +20,35 @@
#include "Common.h"
#include "Http.h"
#include "LuaAPI.h"
#include "LuaPlugin.h"
#include "Plugin.h"
#include "PluginManager.h"
#include "Settings.h"
#include "SignalHandling.h"
#include "TConfig.h"
#include "THeartbeatThread.h"
#include <cstdlib>
#include "TLuaEngine.h"
#include "TNetwork.h"
#include "TPPSMonitor.h"
#include "TPluginMonitor.h"
#include "TResourceManager.h"
#include "TServer.h"
#include <cstdint>
#include <iostream>
#include <thread>
static const std::string sCommandlineArguments = R"(
USAGE:
USAGE:
BeamMP-Server [arguments]
ARGUMENTS:
--help
--help
Displays this help and exits.
--port=1234
Sets the server's listening TCP and
UDP port. Overrides ENV and ServerConfig.
--config=/path/to/ServerConfig.toml
Absolute or relative path to the
Absolute or relative path to the
Server Config file, including the
filename. For paths and filenames with
filename. For paths and filenames with
spaces, put quotes around the path.
--working-directory=/path/to/folder
Sets the working directory of the Server.
@@ -51,7 +59,7 @@ ARGUMENTS:
EXAMPLES:
BeamMP-Server --config=../MyWestCoastServerConfig.toml
Runs the BeamMP-Server and uses the server config file
Runs the BeamMP-Server and uses the server config file
which is one directory above it and is named
'MyWestCoastServerConfig.toml'.
)";
@@ -88,6 +96,7 @@ int BeamMPServerMain(MainArguments Arguments) {
Parser.RegisterArgument({ "help" }, ArgsParser::NONE);
Parser.RegisterArgument({ "version" }, ArgsParser::NONE);
Parser.RegisterArgument({ "config" }, ArgsParser::HAS_VALUE);
Parser.RegisterArgument({ "port" }, ArgsParser::HAS_VALUE);
Parser.RegisterArgument({ "working-directory" }, ArgsParser::HAS_VALUE);
Parser.Parse(Arguments.List);
if (!Parser.Verify()) {
@@ -102,9 +111,6 @@ int BeamMPServerMain(MainArguments Arguments) {
return 0;
}
// badly seed C's rng - this is only because rand() is used here and there for unimportant stuff
std::srand(std::time(0));
std::string ConfigPath = "ServerConfig.toml";
if (Parser.FoundArgument({ "config" })) {
auto MaybeConfigPath = Parser.GetValueOfArgument({ "config" });
@@ -135,6 +141,24 @@ int BeamMPServerMain(MainArguments Arguments) {
}
return 1;
}
// override port if provided via arguments
if (Parser.FoundArgument({ "port" })) {
auto Port = Parser.GetValueOfArgument({ "port" });
if (Port.has_value()) {
auto P = int(std::strtoul(Port.value().c_str(), nullptr, 10));
if (P == 0 || P < 0 || P > UINT16_MAX) {
beammp_errorf("Custom port requested via --port is invalid: '{}'", Port.value());
return 1;
} else {
Application::Settings.set(Settings::Key::General_Port, P);
beammp_info("Custom port requested via commandline arguments: " + Port.value());
}
}
}
Config.PrintDebug();
Application::InitializeConsole();
Application::Console().StartLoggingToFile();
@@ -142,48 +166,40 @@ int BeamMPServerMain(MainArguments Arguments) {
SetupSignalHandlers();
Settings settings {};
beammp_infof("Server name set in new impl: {}", settings.getAsString(Settings::Key::General_Name));
bool Shutdown = false;
Application::RegisterShutdownHandler([&Shutdown] {
beammp_info("If this takes too long, you can press Ctrl+C repeatedly to force a shutdown.");
Application::SetSubsystemStatus("Main", Application::Status::ShuttingDown);
Shutdown = true;
});
/*
Application::RegisterShutdownHandler([] {
auto Futures = LuaAPI::MP::Engine->TriggerEvent("onShutdown", "");
TLuaEngine::WaitForAll(Futures, std::chrono::seconds(5));
});
Application::Console().InitializeLuaConsole(*LuaEngine);
*/
TServer Server(Arguments.List);
RegisterThread("Main");
beammp_trace("Running in debug mode on a debug build");
std::shared_ptr<Network> network = std::make_shared<Network>();
THeartbeatThread Heartbeat(network);
// LuaEngine->SetNetwork(network);
TResourceManager ResourceManager;
ResourceManager.RefreshFiles();
TPPSMonitor PPSMonitor(Server);
THeartbeatThread Heartbeat(ResourceManager, Server);
TNetwork Network(Server, PPSMonitor, ResourceManager);
auto LuaEngine = std::make_shared<TLuaEngine>();
LuaEngine->SetServer(&Server);
Application::Console().InitializeLuaConsole(*LuaEngine);
LuaEngine->SetNetwork(&Network);
PPSMonitor.SetNetwork(Network);
Application::CheckForUpdates();
// TPluginMonitor PluginMonitor(fs::path(Application::Settings.Resource) / "Server", LuaEngine);
PluginManager PluginManager;
(void)PluginManager.add_plugin(Plugin::make_pointer<LuaPlugin>("Resources/Server/Test"));
auto console = Plugin::make_pointer<LuaPlugin>(BEAMMP_MEMORY_STATE);
(void)PluginManager.add_plugin(console);
TPluginMonitor PluginMonitor(fs::path(Application::Settings.getAsString(Settings::Key::General_ResourceFolder)) / "Server", LuaEngine);
dynamic_cast<LuaPlugin&>(*console).run_raw_lua(R"(local n = 12; return n)");
PluginManager.trigger_event("onInit", std::make_shared<Value>(HashMap<std::string, Value> {
{ "big", "balls" },
{ "longer", "falls" },
}));
if (Application::Settings.HTTPServerEnabled) {
Http::Server::THttpServerInstance HttpServerInstance {};
}
Application::SetSubsystemStatus("Main", Application::Status::Good);
RegisterThread("Main(Waiting)");
std::set<std::string> IgnoreSubsystems {
@@ -198,6 +214,10 @@ int BeamMPServerMain(MainArguments Arguments) {
std::string SystemsBadList {};
auto Statuses = Application::GetSubsystemStatuses();
for (const auto& NameStatusPair : Statuses) {
if (NameStatusPair.first == "Main") {
continue;
}
if (IgnoreSubsystems.count(NameStatusPair.first) > 0) {
continue; // ignore
}
@@ -211,6 +231,8 @@ int BeamMPServerMain(MainArguments Arguments) {
// remove ", "
SystemsBadList = SystemsBadList.substr(0, SystemsBadList.size() - 2);
if (FullyStarted) {
Application::SetSubsystemStatus("Main", Application::Status::Good);
if (!WithErrors) {
beammp_info("ALL SYSTEMS STARTED SUCCESSFULLY, EVERYTHING IS OKAY");
} else {
+66
View File
@@ -0,0 +1,66 @@
local function assert_eq(x, y, explain)
if x ~= y then
print("assertion '"..explain.."' failed:\n\tgot:\t", x, "\n\texpected:", y)
end
end
---@param o1 any|table First object to compare
---@param o2 any|table Second object to compare
---@param ignore_mt boolean True to ignore metatables (a recursive function to tests tables inside tables)
function equals(o1, o2, ignore_mt)
if o1 == o2 then return true end
local o1Type = type(o1)
local o2Type = type(o2)
if o1Type ~= o2Type then return false end
if o1Type ~= 'table' then return false end
if not ignore_mt then
local mt1 = getmetatable(o1)
if mt1 and mt1.__eq then
--compare using built in method
return o1 == o2
end
end
local keySet = {}
for key1, value1 in pairs(o1) do
local value2 = o2[key1]
if value2 == nil or equals(value1, value2, ignore_mt) == false then
return false
end
keySet[key1] = true
end
for key2, _ in pairs(o2) do
if not keySet[key2] then return false end
end
return true
end
local function assert_table_eq(x, y, explain)
if not equals(x, y, true) then
print("assertion '"..explain.."' failed:\n\tgot:\t", x, "\n\texpected:", y)
end
end
assert_eq(Util.JsonEncode({1, 2, 3, 4, 5}), "[1,2,3,4,5]", "table to array")
assert_eq(Util.JsonEncode({"a", 1, 2, 3, 4, 5}), '["a",1,2,3,4,5]', "table to array")
assert_eq(Util.JsonEncode({"a", 1, 2.0, 3, 4, 5}), '["a",1,2.0,3,4,5]', "table to array")
assert_eq(Util.JsonEncode({hello="world", john={doe = 1, jane = 2.5, mike = {2, 3, 4}}, dave={}}), '{"dave":{},"hello":"world","john":{"doe":1,"jane":2.5,"mike":[2,3,4]}}', "table to obj")
assert_eq(Util.JsonEncode({a = nil}), "{}", "null obj member")
assert_eq(Util.JsonEncode({1, nil, 3}), "[1,3]", "null array member")
assert_eq(Util.JsonEncode({}), "{}", "empty array/table")
assert_eq(Util.JsonEncode({1234}), "[1234]", "int")
assert_eq(Util.JsonEncode({1234.0}), "[1234.0]", "double")
assert_table_eq(Util.JsonDecode("[1,2,3,4,5]"), {1, 2, 3, 4, 5}, "decode table to array")
assert_table_eq(Util.JsonDecode('["a",1,2,3,4,5]'), {"a", 1, 2, 3, 4, 5}, "decode table to array")
assert_table_eq(Util.JsonDecode('["a",1,2.0,3,4,5]'), {"a", 1, 2.0, 3, 4, 5}, "decode table to array")
assert_table_eq(Util.JsonDecode('{"dave":{},"hello":"world","john":{"doe":1,"jane":2.5,"mike":[2,3,4]}}'), {hello="world", john={doe = 1, jane = 2.5, mike = {2, 3, 4}}, dave={}}, "decode table to obj")
assert_table_eq(Util.JsonDecode("{}"), {a = nil}, "decode null obj member")
assert_table_eq(Util.JsonDecode("[1,3]"), {1, 3}, "decode null array member")
assert_table_eq(Util.JsonDecode("{}"), {}, "decode empty array/table")
assert_table_eq(Util.JsonDecode("[1234]"), {1234}, "decode int")
assert_table_eq(Util.JsonDecode("[1234.0]"), {1234.0}, "decode double")
+1 -1
Submodule vcpkg updated: 8397227251...5bf0c55239
+15 -6
View File
@@ -6,17 +6,26 @@
"boost-spirit",
"boost-uuid",
"boost-variant",
"boost-iostreams",
"boost-json",
"cpp-httplib",
"doctest",
"fmt",
"libzip",
"nlohmann-json",
"openssl",
"rapidjson",
"sol2",
"toml11",
"zstd",
"glm"
]
"curl",
"lua"
],
"overrides": [
{
"name": "sol2",
"version": "3.3.1"
},
{
"name": "lua",
"version": "5.3.5#6"
}
],
"builtin-baseline": "5bf0c55239da398b8c6f450818c9e28d36bf9966"
}