Compare commits

...

42 Commits

Author SHA1 Message Date
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
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
26 changed files with 413 additions and 120 deletions

View File

@ -84,7 +84,7 @@ jobs:
run: ./bin/BeamMP-Server-tests run: ./bin/BeamMP-Server-tests
arm64-matrix: arm64-matrix:
runs-on: [Linux, ARM64] runs-on: ubuntu-22.04-arm
env: env:
VCPKG_DEFAULT_TRIPLET: "arm64-linux" VCPKG_DEFAULT_TRIPLET: "arm64-linux"
strategy: strategy:

View File

@ -104,7 +104,7 @@ jobs:
asset_content_type: application/x-elf asset_content_type: application/x-elf
arm64-matrix: arm64-matrix:
runs-on: [Linux, ARM64] runs-on: ubuntu-22.04-arm
needs: create-release needs: create-release
strategy: strategy:
matrix: matrix:

View File

@ -56,14 +56,14 @@ public:
~TClient(); ~TClient();
TClient& operator=(const TClient&) = delete; TClient& operator=(const TClient&) = delete;
void AddNewCar(int Ident, const std::string& Data); void AddNewCar(int Ident, const nlohmann::json& Data);
void SetCarData(int Ident, const std::string& Data); void SetCarData(int Ident, const nlohmann::json& Data);
void SetCarPosition(int Ident, const std::string& Data); void SetCarPosition(int Ident, const std::string& Data);
TVehicleDataLockPair GetAllCars(); TVehicleDataLockPair GetAllCars();
void SetName(const std::string& Name) { mName = Name; } void SetName(const std::string& Name) { mName = Name; }
void SetRoles(const std::string& Role) { mRole = Role; } void SetRoles(const std::string& Role) { mRole = Role; }
void SetIdentifier(const std::string& key, const std::string& value) { mIdentifiers[key] = value; } void SetIdentifier(const std::string& key, const std::string& value) { mIdentifiers[key] = value; }
std::string GetCarData(int Ident); nlohmann::json GetCarData(int Ident);
std::string GetCarPositionRaw(int Ident); std::string GetCarPositionRaw(int Ident);
void SetUDPAddr(const ip::udp::endpoint& Addr) { mUDPAddress = Addr; } void SetUDPAddr(const ip::udp::endpoint& Addr) { mUDPAddress = Addr; }
void SetTCPSock(ip::tcp::socket&& CSock) { mSocket = std::move(CSock); } void SetTCPSock(ip::tcp::socket&& CSock) { mSocket = std::move(CSock); }

View File

@ -86,6 +86,8 @@ public:
}; };
} }
static std::string GetServerCheckUrl() { return "https://check.beammp.com"; }
static std::string GetBackendUrlForAuth() { return "https://auth.beammp.com"; } static std::string GetBackendUrlForAuth() { return "https://auth.beammp.com"; }
static std::string GetBackendUrlForSocketIO() { return "https://backend.beammp.com"; } static std::string GetBackendUrlForSocketIO() { return "https://backend.beammp.com"; }
static void CheckForUpdates(); static void CheckForUpdates();
@ -127,10 +129,11 @@ private:
static inline std::mutex mShutdownHandlersMutex {}; static inline std::mutex mShutdownHandlersMutex {};
static inline std::deque<TShutdownHandler> mShutdownHandlers {}; static inline std::deque<TShutdownHandler> mShutdownHandlers {};
static inline Version mVersion { 3, 8, 0 }; static inline Version mVersion { 3, 8, 4 };
}; };
void SplitString(std::string const& str, const char delim, std::vector<std::string>& out); 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); std::string ThreadName(bool DebugModeOverride = false);
void RegisterThread(const std::string& str); void RegisterThread(const std::string& str);

View File

@ -27,6 +27,7 @@ enum class Key {
PROVIDER_UPDATE_MESSAGE, PROVIDER_UPDATE_MESSAGE,
PROVIDER_DISABLE_CONFIG, PROVIDER_DISABLE_CONFIG,
PROVIDER_PORT_ENV, PROVIDER_PORT_ENV,
PROVIDER_IP_ENV
}; };
std::optional<std::string> Get(Key key); std::optional<std::string> Get(Key key);

View File

@ -36,6 +36,7 @@ namespace MP {
std::pair<bool, std::string> DropPlayer(int ID, std::optional<std::string> MaybeReason); 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> 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> 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); std::pair<bool, std::string> RemoveVehicle(int PlayerID, int VehicleID);
void Set(int ConfigID, sol::object NewValue); void Set(int ConfigID, sol::object NewValue);
TLuaValue Get(int ConfigID); TLuaValue Get(int ConfigID);

View File

@ -79,6 +79,7 @@ struct Settings {
General_Map, General_Map,
General_AuthKey, General_AuthKey,
General_Private, General_Private,
General_IP,
General_Port, General_Port,
General_MaxCars, General_MaxCars,
General_LogChat, General_LogChat,

View File

@ -59,6 +59,9 @@ private:
void Command_Settings(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_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_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); void Command_Say(const std::string& FullCommand);
bool EnsureArgsCount(const std::vector<std::string>& args, size_t n); bool EnsureArgsCount(const std::vector<std::string>& args, size_t n);
@ -77,6 +80,9 @@ private:
{ "clear", [this](const auto& a, const auto& b) { Command_Clear(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 { "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); } }, { "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::unique_ptr<Commandline> mCommandline { nullptr };

View File

@ -45,6 +45,8 @@ public:
void SendToAll(TClient* c, const std::vector<uint8_t>& Data, bool Self, bool Rel); void SendToAll(TClient* c, const std::vector<uint8_t>& Data, bool Self, bool Rel);
void UpdatePlayer(TClient& Client); void UpdatePlayer(TClient& Client);
TResourceManager& ResourceManager() const { return mResourceManager; }
private: private:
void UDPServerMain(); void UDPServerMain();
void TCPServerMain(); void TCPServerMain();

View File

@ -30,10 +30,10 @@ public:
[[nodiscard]] std::string TrimmedList() const { return mTrimmedList; } [[nodiscard]] std::string TrimmedList() const { return mTrimmedList; }
[[nodiscard]] std::string FileSizes() const { return mFileSizes; } [[nodiscard]] std::string FileSizes() const { return mFileSizes; }
[[nodiscard]] int ModsLoaded() const { return mModsLoaded; } [[nodiscard]] int ModsLoaded() const { return mModsLoaded; }
[[nodiscard]] nlohmann::json GetMods() const { return mMods; }
[[nodiscard]] std::string NewFileList() const;
void RefreshFiles(); void RefreshFiles();
void SetProtected(const std::string& ModName, bool Protected);
private: private:
size_t mMaxModSize = 0; size_t mMaxModSize = 0;

View File

@ -18,11 +18,12 @@
#pragma once #pragma once
#include <nlohmann/json.hpp>
#include <string> #include <string>
class TVehicleData final { class TVehicleData final {
public: public:
TVehicleData(int ID, std::string Data); TVehicleData(int ID, nlohmann::json Data);
~TVehicleData(); ~TVehicleData();
// We cannot delete this, since vector needs to be able to copy when it resizes. // 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, // 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]] bool IsInvalid() const { return mID == -1; }
[[nodiscard]] int ID() const { return mID; } [[nodiscard]] int ID() const { return mID; }
[[nodiscard]] std::string Data() const { return mData; } [[nodiscard]] nlohmann::json Data() const { return mData; }
void SetData(const std::string& Data) { mData = Data; } [[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; } bool operator==(const TVehicleData& v) const { return mID == v.mID; }
private: private:
int mID { -1 }; int mID { -1 };
std::string mData; nlohmann::json mData;
}; };
// TODO: unused now, remove? // TODO: unused now, remove?

View File

@ -57,7 +57,7 @@ int TClient::GetOpenCarID() const {
return OpenID; return OpenID;
} }
void TClient::AddNewCar(int Ident, const std::string& Data) { void TClient::AddNewCar(int Ident, const nlohmann::json& Data) {
std::unique_lock lock(mVehicleDataMutex); std::unique_lock lock(mVehicleDataMutex);
mVehicleData.emplace_back(Ident, Data); mVehicleData.emplace_back(Ident, Data);
} }
@ -98,7 +98,7 @@ void TClient::SetCarPosition(int Ident, const std::string& Data) {
mVehiclePosition[size_t(Ident)] = Data; mVehiclePosition[size_t(Ident)] = Data;
} }
std::string TClient::GetCarData(int Ident) { nlohmann::json TClient::GetCarData(int Ident) {
{ // lock { // lock
std::unique_lock lock(mVehicleDataMutex); std::unique_lock lock(mVehicleDataMutex);
for (auto& v : mVehicleData) { for (auto& v : mVehicleData) {
@ -108,10 +108,10 @@ std::string TClient::GetCarData(int Ident) {
} }
} // unlock } // unlock
DeleteCar(Ident); DeleteCar(Ident);
return ""; return nlohmann::detail::value_t::null;
} }
void TClient::SetCarData(int Ident, const std::string& Data) { void TClient::SetCarData(int Ident, const nlohmann::json& Data) {
{ // lock { // lock
std::unique_lock lock(mVehicleDataMutex); std::unique_lock lock(mVehicleDataMutex);
for (auto& v : mVehicleData) { for (auto& v : mVehicleData) {

View File

@ -384,6 +384,13 @@ void SplitString(const std::string& str, const char delim, std::vector<std::stri
out.push_back(str.substr(start, end - start)); 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 STARTING_MAX_DECOMPRESSION_BUFFER_SIZE = 15 * 1024 * 1024;
static constexpr size_t MAX_DECOMPRESSION_BUFFER_SIZE = 30 * 1024 * 1024; static constexpr size_t MAX_DECOMPRESSION_BUFFER_SIZE = 30 * 1024 * 1024;

View File

@ -39,6 +39,9 @@ std::string_view Env::ToString(Env::Key key) {
case Key::PROVIDER_PORT_ENV: case Key::PROVIDER_PORT_ENV:
return "BEAMMP_PROVIDER_PORT_ENV"; return "BEAMMP_PROVIDER_PORT_ENV";
break; break;
case Key::PROVIDER_IP_ENV:
return "BEAMMP_PROVIDER_IP_ENV";
break;
} }
return ""; return "";
} }

View File

@ -44,6 +44,7 @@ std::string Http::GET(const std::string& url, unsigned int* status) {
CURLcode res; CURLcode res;
char errbuf[CURL_ERROR_SIZE]; char errbuf[CURL_ERROR_SIZE];
curl_easy_setopt(curl, CURLOPT_URL, url.c_str()); 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_WRITEFUNCTION, CurlWriteCallback);
curl_easy_setopt(curl, CURLOPT_WRITEDATA, (void*)&Ret); curl_easy_setopt(curl, CURLOPT_WRITEDATA, (void*)&Ret);
curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT, 10); // seconds curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT, 10); // seconds
@ -75,6 +76,7 @@ std::string Http::POST(const std::string& url, const std::string& body, const st
CURLcode res; CURLcode res;
char errbuf[CURL_ERROR_SIZE]; char errbuf[CURL_ERROR_SIZE];
curl_easy_setopt(curl, CURLOPT_URL, url.c_str()); 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_WRITEFUNCTION, CurlWriteCallback);
curl_easy_setopt(curl, CURLOPT_WRITEDATA, (void*)&Ret); curl_easy_setopt(curl, CURLOPT_WRITEDATA, (void*)&Ret);
curl_easy_setopt(curl, CURLOPT_POST, 1); curl_easy_setopt(curl, CURLOPT_POST, 1);

View File

@ -148,6 +148,11 @@ static inline std::pair<bool, std::string> InternalTriggerClientEvent(int Player
return { false, "Invalid Player ID" }; return { false, "Invalid Player ID" };
} }
auto c = MaybeClient.value().lock(); 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)) { if (!LuaAPI::MP::Engine->Network().Respond(*c, StringToVector(Packet), true)) {
beammp_lua_errorf("Respond failed, dropping client {}", PlayerID); beammp_lua_errorf("Respond failed, dropping client {}", PlayerID);
LuaAPI::MP::Engine->Network().ClientKick(*c, "Disconnected after failing to receive packets"); LuaAPI::MP::Engine->Network().ClientKick(*c, "Disconnected after failing to receive packets");
@ -236,6 +241,48 @@ std::pair<bool, std::string> LuaAPI::MP::SendNotification(int ID, const std::str
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) { std::pair<bool, std::string> LuaAPI::MP::RemoveVehicle(int PID, int VID) {
std::pair<bool, std::string> Result; std::pair<bool, std::string> Result;
auto MaybeClient = GetClient(Engine->Server(), PID); auto MaybeClient = GetClient(Engine->Server(), PID);
@ -246,7 +293,7 @@ std::pair<bool, std::string> LuaAPI::MP::RemoveVehicle(int PID, int VID) {
return Result; return Result;
} }
auto c = MaybeClient.value().lock(); auto c = MaybeClient.value().lock();
if (!c->GetCarData(VID).empty()) { if (c->GetCarData(VID) != nlohmann::detail::value_t::null) {
std::string Destroy = "Od:" + std::to_string(PID) + "-" + std::to_string(VID); std::string Destroy = "Od:" + std::to_string(PID) + "-" + std::to_string(VID);
LuaAPI::MP::Engine->ReportErrors(LuaAPI::MP::Engine->TriggerEvent("onVehicleDeleted", "", PID, VID)); LuaAPI::MP::Engine->ReportErrors(LuaAPI::MP::Engine->TriggerEvent("onVehicleDeleted", "", PID, VID));
Engine->Network().SendToAll(nullptr, StringToVector(Destroy), true, true); Engine->Network().SendToAll(nullptr, StringToVector(Destroy), true, true);

View File

@ -28,6 +28,7 @@ Settings::Settings() {
{ General_Map, std::string("/levels/gridmap_v2/info.json") }, { General_Map, std::string("/levels/gridmap_v2/info.json") },
{ General_AuthKey, std::string("") }, { General_AuthKey, std::string("") },
{ General_Private, true }, { General_Private, true },
{ General_IP, "::"},
{ General_Port, 30814 }, { General_Port, 30814 },
{ General_MaxCars, 1 }, { General_MaxCars, 1 },
{ General_LogChat, true }, { General_LogChat, true },
@ -47,6 +48,7 @@ Settings::Settings() {
{ { "General", "Map" }, { General_Map, READ_WRITE } }, { { "General", "Map" }, { General_Map, READ_WRITE } },
{ { "General", "AuthKey" }, { General_AuthKey, NO_ACCESS } }, { { "General", "AuthKey" }, { General_AuthKey, NO_ACCESS } },
{ { "General", "Private" }, { General_Private, READ_ONLY } }, { { "General", "Private" }, { General_Private, READ_ONLY } },
{ { "General", "IP" }, { General_IP, READ_ONLY } },
{ { "General", "Port" }, { General_Port, READ_ONLY } }, { { "General", "Port" }, { General_Port, READ_ONLY } },
{ { "General", "MaxCars" }, { General_MaxCars, READ_WRITE } }, { { "General", "MaxCars" }, { General_MaxCars, READ_WRITE } },
{ { "General", "LogChat" }, { General_LogChat, READ_ONLY } }, { { "General", "LogChat" }, { General_LogChat, READ_ONLY } },

View File

@ -34,6 +34,8 @@ static constexpr std::string_view StrDebug = "Debug";
static constexpr std::string_view EnvStrDebug = "BEAMMP_DEBUG"; static constexpr std::string_view EnvStrDebug = "BEAMMP_DEBUG";
static constexpr std::string_view StrPrivate = "Private"; static constexpr std::string_view StrPrivate = "Private";
static constexpr std::string_view EnvStrPrivate = "BEAMMP_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 StrPort = "Port";
static constexpr std::string_view EnvStrPort = "BEAMMP_PORT"; static constexpr std::string_view EnvStrPort = "BEAMMP_PORT";
static constexpr std::string_view StrMaxCars = "MaxCars"; static constexpr std::string_view StrMaxCars = "MaxCars";
@ -62,7 +64,9 @@ static constexpr std::string_view StrPassword = "Password";
// Misc // Misc
static constexpr std::string_view StrHideUpdateMessages = "ImScaredOfUpdates"; 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 StrUpdateReminderTime = "UpdateReminderTime";
static constexpr std::string_view EnvStrUpdateReminderTime = "BEAMMP_UPDATE_REMINDER_TIME";
TEST_CASE("TConfig::TConfig") { TEST_CASE("TConfig::TConfig") {
const std::string CfgFile = "beammp_server_testconfig.toml"; const std::string CfgFile = "beammp_server_testconfig.toml";
@ -136,6 +140,8 @@ void TConfig::FlushToFile() {
data["General"][StrInformationPacket.data()] = Application::Settings.getAsBool(Settings::Key::General_InformationPacket); data["General"][StrInformationPacket.data()] = Application::Settings.getAsBool(Settings::Key::General_InformationPacket);
data["General"][StrAllowGuests.data()] = Application::Settings.getAsBool(Settings::Key::General_AllowGuests); data["General"][StrAllowGuests.data()] = Application::Settings.getAsBool(Settings::Key::General_AllowGuests);
SetComment(data["General"][StrAllowGuests.data()].comments(), " Whether to allow guests"); 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"][StrPort.data()] = Application::Settings.getAsInt(Settings::Key::General_Port);
data["General"][StrName.data()] = Application::Settings.getAsString(Settings::Key::General_Name); 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."); 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.");
@ -252,6 +258,11 @@ void TConfig::ParseFromFile(std::string_view name) {
} else { } else {
TryReadValue(data, "General", StrPort, EnvStrPort, Settings::Key::General_Port); 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", StrMaxCars, EnvStrMaxCars, Settings::Key::General_MaxCars);
TryReadValue(data, "General", StrMaxPlayers, EnvStrMaxPlayers, Settings::Key::General_MaxPlayers); TryReadValue(data, "General", StrMaxPlayers, EnvStrMaxPlayers, Settings::Key::General_MaxPlayers);
TryReadValue(data, "General", StrMap, EnvStrMap, Settings::Key::General_Map); TryReadValue(data, "General", StrMap, EnvStrMap, Settings::Key::General_Map);
@ -263,8 +274,8 @@ void TConfig::ParseFromFile(std::string_view name) {
TryReadValue(data, "General", StrLogChat, EnvStrLogChat, Settings::Key::General_LogChat); TryReadValue(data, "General", StrLogChat, EnvStrLogChat, Settings::Key::General_LogChat);
TryReadValue(data, "General", StrAllowGuests, EnvStrAllowGuests, Settings::Key::General_AllowGuests); TryReadValue(data, "General", StrAllowGuests, EnvStrAllowGuests, Settings::Key::General_AllowGuests);
// Misc // Misc
TryReadValue(data, "Misc", StrHideUpdateMessages, "", Settings::Key::Misc_ImScaredOfUpdates); TryReadValue(data, "Misc", StrHideUpdateMessages, EnvStrHideUpdateMessages, Settings::Key::Misc_ImScaredOfUpdates);
TryReadValue(data, "Misc", StrUpdateReminderTime, "", Settings::Key::Misc_UpdateReminderTime); TryReadValue(data, "Misc", StrUpdateReminderTime, EnvStrUpdateReminderTime, Settings::Key::Misc_UpdateReminderTime);
} catch (const std::exception& err) { } catch (const std::exception& err) {
beammp_error("Error parsing config file value: " + std::string(err.what())); beammp_error("Error parsing config file value: " + std::string(err.what()));
@ -302,6 +313,7 @@ void TConfig::PrintDebug() {
beammp_debug(std::string(StrPrivate) + ": " + std::string(Application::Settings.getAsBool(Settings::Key::General_Private) ? "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(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(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(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(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(StrMap) + ": \"" + Application::Settings.getAsString(Settings::Key::General_Map) + "\"");

View File

@ -24,6 +24,7 @@
#include "CustomAssert.h" #include "CustomAssert.h"
#include "LuaAPI.h" #include "LuaAPI.h"
#include "TLuaEngine.h" #include "TLuaEngine.h"
#include "Http.h"
#include <ctime> #include <ctime>
#include <lua.hpp> #include <lua.hpp>
@ -208,16 +209,18 @@ void TConsole::Command_Help(const std::string&, const std::vector<std::string>&
} }
static constexpr const char* sHelpString = R"( static constexpr const char* sHelpString = R"(
Commands: Commands:
help displays this help help displays this help
exit shuts down the server exit shuts down the server
kick <name> [reason] kicks specified player with an optional reason kick <name> [reason] kicks specified player with an optional reason
list lists all players and info about them list lists all players and info about them
say <message> sends the message to all players in chat say <message> sends the message to all players in chat
lua [state id] switches to lua, optionally into a specific state id's lua 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 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 status how the server is doing and what it's up to
clear clears the console window clear clears the console window
version displays the server version)"; 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)); Application::Console().WriteRaw("BeamMP-Server Console: " + std::string(sHelpString));
} }
@ -262,6 +265,56 @@ void TConsole::Command_Version(const std::string& cmd, const std::vector<std::st
std::string openssl_version = fmt::format("OpenSSL: v{}.{}.{}", OPENSSL_VERSION_MAJOR, OPENSSL_VERSION_MINOR, OPENSSL_VERSION_PATCH); std::string openssl_version = fmt::format("OpenSSL: v{}.{}.{}", OPENSSL_VERSION_MAJOR, OPENSSL_VERSION_MINOR, OPENSSL_VERSION_PATCH);
Application::Console().WriteRaw(openssl_version); 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) { void TConsole::Command_Kick(const std::string&, const std::vector<std::string>& args) {
if (!EnsureArgsCount(args, 1, size_t(-1))) { if (!EnsureArgsCount(args, 1, size_t(-1))) {

View File

@ -40,18 +40,6 @@ TLuaEngine* LuaAPI::MP::Engine;
static sol::protected_function AddTraceback(sol::state_view StateView, sol::protected_function RawFn); static sol::protected_function AddTraceback(sol::state_view StateView, sol::protected_function RawFn);
static std::optional<sol::function> GetLuaHandler(sol::state_view StateView, const std::string Handler, const std::string EventName);
static std::optional<sol::function> GetLuaHandler(sol::state_view StateView, const std::string Handler, const std::string EventName) {
auto Res = StateView.safe_script("return " + Handler, sol::script_pass_on_error);
if (!Res.valid()) {
beammp_errorf("invalid handler for event \"{}\". handler: \"{}\"", EventName, Handler);
} else if (Res.get_type() == sol::type::function) {
return Res.get<sol::function>();
}
return std::nullopt;
}
TLuaEngine::TLuaEngine() TLuaEngine::TLuaEngine()
: mResourceServerPath(fs::path(Application::Settings.getAsString(Settings::Key::General_ResourceFolder)) / "Server") { : mResourceServerPath(fs::path(Application::Settings.getAsString(Settings::Key::General_ResourceFolder)) / "Server") {
Application::SetSubsystemStatus("LuaEngine", Application::Status::Starting); Application::SetSubsystemStatus("LuaEngine", Application::Status::Starting);
@ -80,10 +68,11 @@ TEST_CASE("TLuaEngine ctor & dtor") {
void TLuaEngine::operator()() { void TLuaEngine::operator()() {
RegisterThread("LuaEngine"); RegisterThread("LuaEngine");
Application::SetSubsystemStatus("LuaEngine", Application::Status::Good);
// lua engine main thread // lua engine main thread
beammp_infof("Lua v{}.{}.{}", LUA_VERSION_MAJOR, LUA_VERSION_MINOR, LUA_VERSION_RELEASE); beammp_infof("Lua v{}.{}.{}", LUA_VERSION_MAJOR, LUA_VERSION_MINOR, LUA_VERSION_RELEASE);
CollectAndInitPlugins(); CollectAndInitPlugins();
Application::SetSubsystemStatus("LuaEngine", Application::Status::Good);
// now call all onInit's // now call all onInit's
auto Futures = TriggerEvent("onInit", ""); auto Futures = TriggerEvent("onInit", "");
WaitForAll(Futures, std::chrono::seconds(5)); WaitForAll(Futures, std::chrono::seconds(5));
@ -506,11 +495,9 @@ sol::table TLuaEngine::StateThreadData::Lua_TriggerGlobalEvent(const std::string
sol::variadic_results LocalArgs = JsonStringToArray(Str); sol::variadic_results LocalArgs = JsonStringToArray(Str);
for (const auto& Handler : MyHandlers) { for (const auto& Handler : MyHandlers) {
auto Res = GetLuaHandler(mStateView, Handler, EventName); auto Fn = mStateView[Handler];
if (Res.has_value()) { Fn = AddTraceback(mStateView, Fn);
sol::function Fn = Res.value(); if (Fn.valid()) {
Fn = AddTraceback(mStateView, Fn);
auto LuaResult = Fn(LocalArgs); auto LuaResult = Fn(LocalArgs);
auto Result = std::make_shared<TLuaResult>(); auto Result = std::make_shared<TLuaResult>();
if (LuaResult.valid()) { if (LuaResult.valid()) {
@ -562,9 +549,8 @@ sol::table TLuaEngine::StateThreadData::Lua_TriggerLocalEvent(const std::string&
sol::table Result = mStateView.create_table(); sol::table Result = mStateView.create_table();
int i = 1; int i = 1;
for (const auto& Handler : mEngine->GetEventHandlersForState(EventName, mStateId)) { for (const auto& Handler : mEngine->GetEventHandlersForState(EventName, mStateId)) {
auto Res = GetLuaHandler(mStateView, Handler, EventName); auto Fn = mStateView[Handler];
if (Res.has_value()) { if (Fn.valid() && Fn.get_type() == sol::type::function) {
sol::function Fn = Res.value();
auto FnRet = Fn(EventArgs); auto FnRet = Fn(EventArgs);
if (FnRet.valid()) { if (FnRet.valid()) {
Result.set(i, FnRet); Result.set(i, FnRet);
@ -682,7 +668,7 @@ sol::table TLuaEngine::StateThreadData::Lua_GetPlayerVehicles(int ID) {
sol::state_view StateView(mState); sol::state_view StateView(mState);
sol::table Result = StateView.create_table(); sol::table Result = StateView.create_table();
for (const auto& v : VehicleData) { for (const auto& v : VehicleData) {
Result[v.ID()] = v.Data().substr(3); Result[v.ID()] = v.DataAsPacket(Client->GetRoles(), Client->GetName(), Client->GetID()).substr(3);
} }
return Result; return Result;
} else } else
@ -890,6 +876,12 @@ TLuaEngine::StateThreadData::StateThreadData(const std::string& Name, TLuaStateI
beammp_lua_error("SendNotification expects 2, 3 or 4 arguments."); beammp_lua_error("SendNotification expects 2, 3 or 4 arguments.");
} }
}); });
MPTable.set_function("ConfirmationDialog", sol::overload(
&LuaAPI::MP::ConfirmationDialog,
[&](const int& ID, const std::string& Title, const std::string& Body, const sol::table& Buttons, const std::string& InteractionID) {
LuaAPI::MP::ConfirmationDialog(ID, Title, Body, Buttons, InteractionID);
}
));
MPTable.set_function("GetPlayers", [&]() -> sol::table { MPTable.set_function("GetPlayers", [&]() -> sol::table {
return Lua_GetPlayers(); return Lua_GetPlayers();
}); });
@ -1176,10 +1168,8 @@ void TLuaEngine::StateThreadData::operator()() {
// TODO: Use TheQueuedFunction.EventName for errors, warnings, etc // TODO: Use TheQueuedFunction.EventName for errors, warnings, etc
Result->StateId = mStateId; Result->StateId = mStateId;
sol::state_view StateView(mState); sol::state_view StateView(mState);
auto RawFn = StateView[FnName];
auto Res = GetLuaHandler(StateView, FnName, TheQueuedFunction.EventName); if (RawFn.valid() && RawFn.get_type() == sol::type::function) {
if (Res.has_value()) {
sol::function Fn = Res.value();
std::vector<sol::object> LuaArgs; std::vector<sol::object> LuaArgs;
for (const auto& Arg : Args) { for (const auto& Arg : Args) {
if (Arg.valueless_by_exception()) { if (Arg.valueless_by_exception()) {
@ -1214,7 +1204,7 @@ void TLuaEngine::StateThreadData::operator()() {
break; break;
} }
} }
Fn = AddTraceback(StateView, Fn); auto Fn = AddTraceback(StateView, RawFn);
auto Res = Fn(sol::as_args(LuaArgs)); auto Res = Fn(sol::as_args(LuaArgs));
if (Res.valid()) { if (Res.valid()) {
Result->Error = false; Result->Error = false;

View File

@ -85,9 +85,17 @@ TNetwork::TNetwork(TServer& Server, TPPSMonitor& PPSMonitor, TResourceManager& R
void TNetwork::UDPServerMain() { void TNetwork::UDPServerMain() {
RegisterThread("UDPServer"); RegisterThread("UDPServer");
// listen on all ipv6 addresses
ip::udp::endpoint UdpListenEndpoint(ip::make_address("::"), Application::Settings.getAsInt(Settings::Key::General_Port));
boost::system::error_code ec; boost::system::error_code ec;
auto address = ip::make_address(Application::Settings.getAsString(Settings::Key::General_IP), ec);
if (ec) {
beammp_errorf("Failed to parse IP: {}", ec.message());
Application::GracefullyShutdown();
}
ip::udp::endpoint UdpListenEndpoint(address, Application::Settings.getAsInt(Settings::Key::General_Port));
mUDPSock.open(UdpListenEndpoint.protocol(), ec); mUDPSock.open(UdpListenEndpoint.protocol(), ec);
if (ec) { if (ec) {
beammp_error("open() failed: " + ec.message()); beammp_error("open() failed: " + ec.message());
@ -107,7 +115,7 @@ void TNetwork::UDPServerMain() {
Application::GracefullyShutdown(); Application::GracefullyShutdown();
} }
Application::SetSubsystemStatus("UDPNetwork", Application::Status::Good); Application::SetSubsystemStatus("UDPNetwork", Application::Status::Good);
beammp_info(("Vehicle data network online on port ") + std::to_string(Application::Settings.getAsInt(Settings::Key::General_Port)) + (" with a Max of ") beammp_info(("Vehicle data network online on port ") + std::to_string(UdpListenEndpoint.port()) + (" with a Max of ")
+ std::to_string(Application::Settings.getAsInt(Settings::Key::General_MaxPlayers)) + (" Clients")); + std::to_string(Application::Settings.getAsInt(Settings::Key::General_MaxPlayers)) + (" Clients"));
while (!Application::IsShuttingDown()) { while (!Application::IsShuttingDown()) {
try { try {
@ -170,12 +178,17 @@ void TNetwork::UDPServerMain() {
void TNetwork::TCPServerMain() { void TNetwork::TCPServerMain() {
RegisterThread("TCPServer"); RegisterThread("TCPServer");
// listen on all ipv6 addresses
auto port = uint16_t(Application::Settings.getAsInt(Settings::Key::General_Port));
ip::tcp::endpoint ListenEp(ip::make_address("::"), port);
beammp_infof("Listening on 0.0.0.0:{0} and [::]:{0}", port);
ip::tcp::socket Listener(mServer.IoCtx());
boost::system::error_code ec; boost::system::error_code ec;
auto address = ip::make_address(Application::Settings.getAsString(Settings::Key::General_IP), ec);
if (ec) {
beammp_errorf("Failed to parse IP: {}", ec.message());
return;
}
ip::tcp::endpoint ListenEp(address,
uint16_t(Application::Settings.getAsInt(Settings::Key::General_Port)));
ip::tcp::socket Listener(mServer.IoCtx());
Listener.open(ListenEp.protocol(), ec); Listener.open(ListenEp.protocol(), ec);
if (ec) { if (ec) {
beammp_errorf("Failed to open socket: {}", ec.message()); beammp_errorf("Failed to open socket: {}", ec.message());
@ -209,6 +222,7 @@ void TNetwork::TCPServerMain() {
Application::GracefullyShutdown(); Application::GracefullyShutdown();
} }
Application::SetSubsystemStatus("TCPNetwork", Application::Status::Good); Application::SetSubsystemStatus("TCPNetwork", Application::Status::Good);
beammp_infof("Listening on {0} port {1}", ListenEp.address().to_string(), static_cast<uint16_t>(ListenEp.port()));
beammp_info("Vehicle event network online"); beammp_info("Vehicle event network online");
do { do {
try { try {
@ -307,6 +321,11 @@ std::shared_ptr<TClient> TNetwork::Authentication(TConnection&& RawConnection) {
Client->SetIdentifier("ip", ip); Client->SetIdentifier("ip", ip);
beammp_tracef("This thread is ip {} ({})", ip, RawConnection.SockAddr.address().to_v6().is_v4_mapped() ? "IPv4 mapped IPv6" : "IPv6"); beammp_tracef("This thread is ip {} ({})", ip, RawConnection.SockAddr.address().to_v6().is_v4_mapped() ? "IPv4 mapped IPv6" : "IPv6");
if (Application::GetSubsystemStatuses().at("Main") == Application::Status::Starting) {
ClientKick(*Client, "The server is still starting, please try joining again later.");
return nullptr;
}
beammp_info("Identifying new ClientConnection..."); beammp_info("Identifying new ClientConnection...");
auto Data = TCPRcv(*Client); auto Data = TCPRcv(*Client);
@ -767,7 +786,7 @@ void TNetwork::Parse(TClient& c, const std::vector<uint8_t>& Packet) {
case 'S': case 'S':
if (SubCode == 'R') { if (SubCode == 'R') {
beammp_debug("Sending Mod Info"); beammp_debug("Sending Mod Info");
std::string ToSend = mResourceManager.NewFileList(); std::string ToSend = mResourceManager.GetMods().dump();
beammp_debugf("Mod Info: {}", ToSend); beammp_debugf("Mod Info: {}", ToSend);
if (!TCPSend(c, StringToVector(ToSend))) { if (!TCPSend(c, StringToVector(ToSend))) {
ClientKick(c, "TCP Send 'SY' failed"); ClientKick(c, "TCP Send 'SY' failed");
@ -789,6 +808,15 @@ void TNetwork::SendFile(TClient& c, const std::string& UnsafeName) {
return; return;
} }
auto FileName = fs::path(UnsafeName).filename().string(); auto FileName = fs::path(UnsafeName).filename().string();
for (auto mod : mResourceManager.GetMods()) {
if (mod["file_name"].get<std::string>() == FileName && mod["protected"] == true) {
beammp_warn("Client tried to access protected file " + UnsafeName);
c.Disconnect("Mod is protected thus cannot be downloaded");
return;
}
}
FileName = Application::Settings.getAsString(Settings::Key::General_ResourceFolder) + "/Client/" + FileName; FileName = Application::Settings.getAsString(Settings::Key::General_ResourceFolder) + "/Client/" + FileName;
if (!std::filesystem::exists(FileName)) { if (!std::filesystem::exists(FileName)) {
@ -946,7 +974,7 @@ bool TNetwork::SyncClient(const std::weak_ptr<TClient>& c) {
res = false; res = false;
return false; return false;
} }
res = Respond(*LockedClient, StringToVector(v.Data()), true, true); res = Respond(*LockedClient, StringToVector(v.DataAsPacket(client->GetRoles(), client->GetName(), client->GetID())), true, true);
} }
} }

View File

@ -57,21 +57,26 @@ void TPluginMonitor::operator()() {
mFileTimes[Pair.first] = CurrentTime; mFileTimes[Pair.first] = CurrentTime;
// grandparent of the path should be Resources/Server // grandparent of the path should be Resources/Server
if (fs::equivalent(fs::path(Pair.first).parent_path().parent_path(), mPath)) { if (fs::equivalent(fs::path(Pair.first).parent_path().parent_path(), mPath)) {
beammp_infof("File \"{}\" changed, reloading", Pair.first); if (LowerString(fs::path(Pair.first).extension().string()) == ".lua") {
// is in root folder, so reload beammp_infof("File \"{}\" changed, reloading", Pair.first);
std::ifstream FileStream(Pair.first, std::ios::in | std::ios::binary); // is in root folder, so reload
auto Size = std::filesystem::file_size(Pair.first); std::ifstream FileStream(Pair.first, std::ios::in | std::ios::binary);
auto Contents = std::make_shared<std::string>(); auto Size = std::filesystem::file_size(Pair.first);
Contents->resize(Size); auto Contents = std::make_shared<std::string>();
FileStream.read(Contents->data(), Contents->size()); Contents->resize(Size);
TLuaChunk Chunk(Contents, Pair.first, fs::path(Pair.first).parent_path().string()); FileStream.read(Contents->data(), Contents->size());
auto StateID = mEngine->GetStateIDForPlugin(fs::path(Pair.first).parent_path()); TLuaChunk Chunk(Contents, Pair.first, fs::path(Pair.first).parent_path().string());
auto Res = mEngine->EnqueueScript(StateID, Chunk); auto StateID = mEngine->GetStateIDForPlugin(fs::path(Pair.first).parent_path());
Res->WaitUntilReady(); auto Res = mEngine->EnqueueScript(StateID, Chunk);
if (Res->Error) { Res->WaitUntilReady();
beammp_lua_errorf("Error while hot-reloading \"{}\": {}", Pair.first, Res->ErrorMessage); 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 { } else {
mEngine->ReportErrors(mEngine->TriggerLocalEvent(StateID, "onInit")); 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)); mEngine->ReportErrors(mEngine->TriggerEvent("onFileChanged", "", Pair.first));
} }
} else { } else {

View File

@ -58,22 +58,58 @@ TResourceManager::TResourceManager() {
Application::SetSubsystemStatus("ResourceManager", Application::Status::Good); Application::SetSubsystemStatus("ResourceManager", Application::Status::Good);
} }
std::string TResourceManager::NewFileList() const {
return mMods.dump();
}
void TResourceManager::RefreshFiles() { void TResourceManager::RefreshFiles() {
mMods.clear(); mMods.clear();
std::unique_lock Lock(mModsMutex); std::unique_lock Lock(mModsMutex);
std::string Path = Application::Settings.getAsString(Settings::Key::General_ResourceFolder) + "/Client"; 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)) { for (const auto& entry : fs::directory_iterator(Path)) {
std::string File(entry.path().string()); 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())) { if (entry.path().extension() != ".zip" || std::filesystem::is_directory(entry.path())) {
beammp_warnf("'{}' is not a ZIP file and will be ignored", File); beammp_warnf("'{}' is not a ZIP file and will be ignored", File);
continue; 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 { try {
EVP_MD_CTX* mdctx; EVP_MD_CTX* mdctx;
const EVP_MD* md; const EVP_MD* md;
@ -133,9 +169,73 @@ void TResourceManager::RefreshFiles() {
{ "file_size", std::filesystem::file_size(File) }, { "file_size", std::filesystem::file_size(File) },
{ "hash_algorithm", "sha256" }, { "hash_algorithm", "sha256" },
{ "hash", result }, { "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) { } catch (const std::exception& e) {
beammp_errorf("Sha256 hashing of '{}' failed: {}", File, e.what()); 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());
}
}
} }

View File

@ -195,6 +195,18 @@ void TServer::GlobalParser(const std::weak_ptr<TClient>& Client, std::vector<uin
// V to Y // V to Y
if (Code <= 89 && Code >= 86) { if (Code <= 89 && Code >= 86) {
int PID = -1;
int VID = -1;
auto MaybePidVid = GetPidVid(StringPacket.substr(3).substr(0, StringPacket.substr(3).find(':', 1)));
if (MaybePidVid) {
std::tie(PID, VID) = MaybePidVid.value();
}
if (PID == -1 || VID == -1 || PID != LockedClient->GetID()) {
return;
}
PPSMonitor.IncrementInternalPPS(); PPSMonitor.IncrementInternalPPS();
Network.SendToAll(LockedClient.get(), Packet, false, false); Network.SendToAll(LockedClient.get(), Packet, false, false);
return; return;
@ -255,11 +267,25 @@ void TServer::GlobalParser(const std::weak_ptr<TClient>& Client, std::vector<uin
case 'N': case 'N':
Network.SendToAll(LockedClient.get(), Packet, false, true); Network.SendToAll(LockedClient.get(), Packet, false, true);
return; return;
case 'Z': // position packet case 'Z': { // position packet
PPSMonitor.IncrementInternalPPS(); PPSMonitor.IncrementInternalPPS();
int PID = -1;
int VID = -1;
auto MaybePidVid = GetPidVid(StringPacket.substr(3).substr(0, StringPacket.substr(3).find(':', 1)));
if (MaybePidVid) {
std::tie(PID, VID) = MaybePidVid.value();
}
if (PID == -1 || VID == -1 || PID != LockedClient->GetID()) {
return;
}
Network.SendToAll(LockedClient.get(), Packet, false, false); Network.SendToAll(LockedClient.get(), Packet, false, false);
HandlePosition(*LockedClient, StringPacket); HandlePosition(*LockedClient, StringPacket);
return; return;
}
default: default:
return; return;
} }
@ -328,8 +354,9 @@ void TServer::ParseVehicle(TClient& c, const std::string& Pckt, TNetwork& Networ
}); });
bool SpawnConfirmed = false; bool SpawnConfirmed = false;
if (ShouldSpawn(c, CarJson, CarID) && !ShouldntSpawn) { auto CarJsonDoc = nlohmann::json::parse(CarJson, nullptr, false);
c.AddNewCar(CarID, Packet); if (ShouldSpawn(c, CarJson, CarID) && !ShouldntSpawn && !CarJsonDoc.is_discarded()) {
c.AddNewCar(CarID, CarJsonDoc);
Network.SendToAll(nullptr, StringToVector(Packet), true, true); Network.SendToAll(nullptr, StringToVector(Packet), true, true);
SpawnConfirmed = true; SpawnConfirmed = true;
} else { } else {
@ -446,6 +473,17 @@ void TServer::ParseVehicle(TClient& c, const std::string& Pckt, TNetwork& Networ
Data = Data.substr(Data.find('[')); Data = Data.substr(Data.find('['));
LuaAPI::MP::Engine->ReportErrors(LuaAPI::MP::Engine->TriggerEvent("onVehiclePaintChanged", "", c.GetID(), VID, Data)); LuaAPI::MP::Engine->ReportErrors(LuaAPI::MP::Engine->TriggerEvent("onVehiclePaintChanged", "", c.GetID(), VID, Data));
Network.SendToAll(&c, StringToVector(Packet), false, true); 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; return;
} }
@ -461,42 +499,22 @@ void TServer::Apply(TClient& c, int VID, const std::string& pckt) {
beammp_error("Malformed packet received, no '{' found"); beammp_error("Malformed packet received, no '{' found");
return; return;
} }
std::string Packet = pckt.substr(FoundPos); std::string Packet = pckt.substr(FoundPos);
std::string VD = c.GetCarData(VID); nlohmann::json VD = c.GetCarData(VID);
if (VD.empty()) { if (VD == nlohmann::detail::value_t::null) {
beammp_error("Tried to apply change to vehicle that does not exist"); beammp_error("Tried to apply change to vehicle that does not exist");
return; return;
} }
std::string Header = VD.substr(0, VD.find('{'));
FoundPos = VD.find('{'); nlohmann::json Pack = nlohmann::json::parse(Packet, nullptr, false);
if (FoundPos == std::string::npos) {
return; if (Pack.is_discarded()) {
}
VD = VD.substr(FoundPos);
rapidjson::Document Veh, Pack;
Veh.Parse(VD.c_str());
if (Veh.HasParseError()) {
beammp_error("Could not get vehicle config!");
return;
}
Pack.Parse(Packet.c_str());
if (Pack.HasParseError() || Pack.IsNull()) {
beammp_error("Could not get active vehicle config!"); beammp_error("Could not get active vehicle config!");
return; return;
} }
for (auto& M : Pack.GetObject()) { c.SetCarData(VID, Pack);
if (Veh[M.name].IsNull()) {
Veh.AddMember(M.name, M.value, Veh.GetAllocator());
} else {
Veh[M.name] = Pack[M.name];
}
}
rapidjson::StringBuffer Buffer;
rapidjson::Writer<rapidjson::StringBuffer> writer(Buffer);
Veh.Accept(writer);
c.SetCarData(VID, Header + Buffer.GetString());
} }
void TServer::InsertClient(const std::shared_ptr<TClient>& NewClient) { void TServer::InsertClient(const std::shared_ptr<TClient>& NewClient) {

View File

@ -21,7 +21,7 @@
#include "Common.h" #include "Common.h"
#include <utility> #include <utility>
TVehicleData::TVehicleData(int ID, std::string Data) TVehicleData::TVehicleData(int ID, nlohmann::json Data)
: mID(ID) : mID(ID)
, mData(std::move(Data)) { , mData(std::move(Data)) {
beammp_trace("vehicle " + std::to_string(mID) + " constructed"); beammp_trace("vehicle " + std::to_string(mID) + " constructed");
@ -30,3 +30,7 @@ TVehicleData::TVehicleData(int ID, std::string Data)
TVehicleData::~TVehicleData() { TVehicleData::~TVehicleData() {
beammp_trace("vehicle " + std::to_string(mID) + " destroyed"); 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();
}

View File

@ -182,10 +182,6 @@ int BeamMPServerMain(MainArguments Arguments) {
TServer Server(Arguments.List); TServer Server(Arguments.List);
auto LuaEngine = std::make_shared<TLuaEngine>();
LuaEngine->SetServer(&Server);
Application::Console().InitializeLuaConsole(*LuaEngine);
RegisterThread("Main"); RegisterThread("Main");
beammp_trace("Running in debug mode on a debug build"); beammp_trace("Running in debug mode on a debug build");
@ -194,13 +190,16 @@ int BeamMPServerMain(MainArguments Arguments) {
TPPSMonitor PPSMonitor(Server); TPPSMonitor PPSMonitor(Server);
THeartbeatThread Heartbeat(ResourceManager, Server); THeartbeatThread Heartbeat(ResourceManager, Server);
TNetwork Network(Server, PPSMonitor, ResourceManager); TNetwork Network(Server, PPSMonitor, ResourceManager);
auto LuaEngine = std::make_shared<TLuaEngine>();
LuaEngine->SetServer(&Server);
Application::Console().InitializeLuaConsole(*LuaEngine);
LuaEngine->SetNetwork(&Network); LuaEngine->SetNetwork(&Network);
PPSMonitor.SetNetwork(Network); PPSMonitor.SetNetwork(Network);
Application::CheckForUpdates(); Application::CheckForUpdates();
TPluginMonitor PluginMonitor(fs::path(Application::Settings.getAsString(Settings::Key::General_ResourceFolder)) / "Server", LuaEngine); TPluginMonitor PluginMonitor(fs::path(Application::Settings.getAsString(Settings::Key::General_ResourceFolder)) / "Server", LuaEngine);
Application::SetSubsystemStatus("Main", Application::Status::Good);
RegisterThread("Main(Waiting)"); RegisterThread("Main(Waiting)");
std::set<std::string> IgnoreSubsystems { std::set<std::string> IgnoreSubsystems {
@ -215,6 +214,10 @@ int BeamMPServerMain(MainArguments Arguments) {
std::string SystemsBadList {}; std::string SystemsBadList {};
auto Statuses = Application::GetSubsystemStatuses(); auto Statuses = Application::GetSubsystemStatuses();
for (const auto& NameStatusPair : Statuses) { for (const auto& NameStatusPair : Statuses) {
if (NameStatusPair.first == "Main") {
continue;
}
if (IgnoreSubsystems.count(NameStatusPair.first) > 0) { if (IgnoreSubsystems.count(NameStatusPair.first) > 0) {
continue; // ignore continue; // ignore
} }
@ -228,6 +231,8 @@ int BeamMPServerMain(MainArguments Arguments) {
// remove ", " // remove ", "
SystemsBadList = SystemsBadList.substr(0, SystemsBadList.size() - 2); SystemsBadList = SystemsBadList.substr(0, SystemsBadList.size() - 2);
if (FullyStarted) { if (FullyStarted) {
Application::SetSubsystemStatus("Main", Application::Status::Good);
if (!WithErrors) { if (!WithErrors) {
beammp_info("ALL SYSTEMS STARTED SUCCESSFULLY, EVERYTHING IS OKAY"); beammp_info("ALL SYSTEMS STARTED SUCCESSFULLY, EVERYTHING IS OKAY");
} else { } else {