Force relay connections (#1813)

* feat(protections): add force relay servers
* feat(network): add support for non-direct connections
* feat(info): add helpful tooltip to prevent unnecessary bug reports
This commit is contained in:
maybegreat48 2023-07-23 16:47:25 +00:00 committed by GitHub
parent 65a5dbd88e
commit 09a189eb4d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 209 additions and 48 deletions

View File

@ -3,7 +3,7 @@ include(FetchContent)
FetchContent_Declare(
gtav_classes
GIT_REPOSITORY https://github.com/Yimura/GTAV-Classes.git
GIT_TAG fe0893ffb589da617442015b97a62c117ac5bed4
GIT_TAG 468161a36d95a355d9783eef5b245f3f1542e2bb
GIT_PROGRESS TRUE
CONFIGURE_COMMAND ""
BUILD_COMMAND ""

View File

@ -0,0 +1,23 @@
#include "backend/looped_command.hpp"
#include "pointers.hpp"
namespace big
{
class force_relay_connections : looped_command
{
using looped_command::looped_command;
virtual void on_tick() override
{
*g_pointers->m_gta.m_force_relay_connections = true;
}
virtual void on_disable() override
{
*g_pointers->m_gta.m_force_relay_connections = false;
}
};
force_relay_connections g_force_relay_connections("forcerelays", "Force Relay Connections", "Hides your IP address by rerouting your connection through dedicated servers and other players",
g.protections.force_relay_connections);
}

View File

@ -274,12 +274,13 @@ namespace big
NLOHMANN_DEFINE_TYPE_INTRUSIVE(script_events, bounty, ceo_money, clear_wanted_level, fake_deposit, force_mission, force_teleport, gta_banner, mc_teleport, personal_vehicle_destroyed, remote_off_radar, rotate_cam, send_to_cutscene, send_to_location, sound_spam, spectate, give_collectible, vehicle_kick, teleport_to_warehouse, start_activity, send_sms)
} script_events{};
bool rid_join = false;
bool receive_pickup = false;
bool admin_check = true;
bool kick_rejoin = true;
bool rid_join = false;
bool receive_pickup = false;
bool admin_check = true;
bool kick_rejoin = true;
bool force_relay_connections = true;
NLOHMANN_DEFINE_TYPE_INTRUSIVE(protections, script_events, rid_join, receive_pickup, admin_check, kick_rejoin)
NLOHMANN_DEFINE_TYPE_INTRUSIVE(protections, script_events, rid_join, receive_pickup, admin_check, kick_rejoin, force_relay_connections)
} protections{};
struct self

View File

@ -16,6 +16,7 @@ namespace rage
class netConnection;
class netMessageQueue;
class netQueuedMessage;
class netConnectionPeer;
class snMsgRemoveGamersFromSessionCmd;
class snSession;
class snPlayer;
@ -120,7 +121,7 @@ namespace big::functions
using request_ragdoll = void (*)(uint16_t object_id);
using request_control = void (*)(rage::netObject* net_object);
using get_peer_address = rage::netPeerAddress* (*)(rage::netConnectionManager* manager, int peer_id);
using get_connection_peer = rage::netConnectionPeer* (*)(rage::netConnectionManager* manager, int peer_id);
using send_remove_gamer_cmd = void (*)(rage::netConnectionManager* net_connection_mgr, rage::netPeerAddress* adde, int connection_id, rage::snMsgRemoveGamersFromSessionCmd* cmd, int flags);
using handle_remove_gamer_cmd = void* (*)(rage::snSession* session, rage::snPlayer* origin, rage::snMsgRemoveGamersFromSessionCmd* cmd);

View File

@ -194,7 +194,7 @@ namespace big
functions::request_control m_request_control;
functions::clear_ped_tasks_network m_clear_ped_tasks_network;
functions::get_peer_address m_get_peer_address;
functions::get_connection_peer m_get_connection_peer;
functions::send_remove_gamer_cmd m_send_remove_gamer_cmd;
functions::handle_remove_gamer_cmd m_handle_remove_gamer_cmd;
@ -298,6 +298,8 @@ namespace big
PVOID m_render_ped;
PVOID m_render_entity;
PVOID m_render_big_ped;
bool* m_force_relay_connections;
};
#pragma pack(pop)
static_assert(sizeof(gta_pointers) % 8 == 0, "Pointers are not properly aligned");

View File

@ -582,7 +582,7 @@ namespace big
constexpr uint32_t crash_vehicles[] = {RAGE_JOAAT("arbitergt"), RAGE_JOAAT("astron2"), RAGE_JOAAT("cyclone2"), RAGE_JOAAT("ignus2"), RAGE_JOAAT("s95")};
constexpr uint32_t crash_objects[] = {RAGE_JOAAT("prop_dummy_01"), RAGE_JOAAT("prop_dummy_car"), RAGE_JOAAT("prop_dummy_light"), RAGE_JOAAT("prop_dummy_plane"), RAGE_JOAAT("prop_distantcar_night"), RAGE_JOAAT("prop_distantcar_day"), RAGE_JOAAT("hei_bh1_08_details4_em_night"), RAGE_JOAAT("dt1_18_sq_night_slod"), RAGE_JOAAT("ss1_12_night_slod"), -1288391198, RAGE_JOAAT("h4_prop_bush_bgnvla_med_01"), RAGE_JOAAT("h4_prop_bush_bgnvla_lrg_01"), RAGE_JOAAT("h4_prop_bush_buddleia_low_01"), RAGE_JOAAT("h4_prop_bush_ear_aa"), RAGE_JOAAT("h4_prop_bush_ear_ab"), RAGE_JOAAT("h4_prop_bush_fern_low_01"), RAGE_JOAAT("h4_prop_bush_fern_tall_cc"), RAGE_JOAAT("h4_prop_bush_mang_ad"), RAGE_JOAAT("h4_prop_bush_mang_low_aa"), RAGE_JOAAT("h4_prop_bush_mang_low_ab"), RAGE_JOAAT("h4_prop_bush_seagrape_low_01"), RAGE_JOAAT("prop_h4_ground_cover"), RAGE_JOAAT("h4_prop_weed_groundcover_01"), RAGE_JOAAT("h4_prop_grass_med_01"), RAGE_JOAAT("h4_prop_grass_tropical_lush_01"), RAGE_JOAAT("h4_prop_grass_wiregrass_01"), RAGE_JOAAT("h4_prop_weed_01_plant"), RAGE_JOAAT("h4_prop_weed_01_row"), RAGE_JOAAT("urbanweeds02_l1"), RAGE_JOAAT("proc_forest_grass01"), RAGE_JOAAT("prop_small_bushyba"), RAGE_JOAAT("v_res_d_dildo_a"), RAGE_JOAAT("v_res_d_dildo_b"), RAGE_JOAAT("v_res_d_dildo_c"), RAGE_JOAAT("v_res_d_dildo_d"), RAGE_JOAAT("v_res_d_dildo_e"), RAGE_JOAAT("v_res_d_dildo_f"), RAGE_JOAAT("v_res_skateboard"), RAGE_JOAAT("prop_battery_01"), RAGE_JOAAT("prop_barbell_01"), RAGE_JOAAT("prop_barbell_02"), RAGE_JOAAT("prop_bandsaw_01"), RAGE_JOAAT("prop_bbq_3"), RAGE_JOAAT("v_med_curtainsnewcloth2"), RAGE_JOAAT("bh1_07_flagpoles"), 92962485};
constexpr uint32_t crash_objects[] = {RAGE_JOAAT("prop_dummy_01"), RAGE_JOAAT("prop_dummy_car"), RAGE_JOAAT("prop_dummy_light"), RAGE_JOAAT("prop_dummy_plane"), RAGE_JOAAT("prop_distantcar_night"), RAGE_JOAAT("prop_distantcar_day"), RAGE_JOAAT("hei_bh1_08_details4_em_night"), RAGE_JOAAT("dt1_18_sq_night_slod"), RAGE_JOAAT("ss1_12_night_slod"), -1288391198, RAGE_JOAAT("h4_prop_bush_bgnvla_med_01"), RAGE_JOAAT("h4_prop_bush_bgnvla_lrg_01"), RAGE_JOAAT("h4_prop_bush_buddleia_low_01"), RAGE_JOAAT("h4_prop_bush_ear_aa"), RAGE_JOAAT("h4_prop_bush_ear_ab"), RAGE_JOAAT("h4_prop_bush_fern_low_01"), RAGE_JOAAT("h4_prop_bush_fern_tall_cc"), RAGE_JOAAT("h4_prop_bush_mang_ad"), RAGE_JOAAT("h4_prop_bush_mang_low_aa"), RAGE_JOAAT("h4_prop_bush_mang_low_ab"), RAGE_JOAAT("h4_prop_bush_seagrape_low_01"), RAGE_JOAAT("prop_h4_ground_cover"), RAGE_JOAAT("h4_prop_weed_groundcover_01"), RAGE_JOAAT("h4_prop_grass_med_01"), RAGE_JOAAT("h4_prop_grass_tropical_lush_01"), RAGE_JOAAT("h4_prop_grass_wiregrass_01"), RAGE_JOAAT("h4_prop_weed_01_plant"), RAGE_JOAAT("h4_prop_weed_01_row"), RAGE_JOAAT("urbanweeds02_l1"), RAGE_JOAAT("proc_forest_grass01"), RAGE_JOAAT("prop_small_bushyba"), RAGE_JOAAT("v_res_d_dildo_a"), RAGE_JOAAT("v_res_d_dildo_b"), RAGE_JOAAT("v_res_d_dildo_c"), RAGE_JOAAT("v_res_d_dildo_d"), RAGE_JOAAT("v_res_d_dildo_e"), RAGE_JOAAT("v_res_d_dildo_f"), RAGE_JOAAT("v_res_skateboard"), RAGE_JOAAT("prop_battery_01"), RAGE_JOAAT("prop_barbell_01"), RAGE_JOAAT("prop_barbell_02"), RAGE_JOAAT("prop_bandsaw_01"), RAGE_JOAAT("prop_bbq_3"), RAGE_JOAAT("v_med_curtainsnewcloth2"), RAGE_JOAAT("bh1_07_flagpoles"), 92962485, RAGE_JOAAT("proc_dry_plants_01"), RAGE_JOAAT("proc_leafyplant_01"), RAGE_JOAAT("proc_grassplantmix_02"), RAGE_JOAAT("proc_dryplantsgrass_01"), RAGE_JOAAT("proc_dryplantsgrass_02"), RAGE_JOAAT("proc_dryplantsgrass_02"), RAGE_JOAAT("proc_grasses01"), RAGE_JOAAT("prop_dryweed_002_a"), RAGE_JOAAT("prop_fernba"), RAGE_JOAAT("prop_weed_001_aa"), RAGE_JOAAT("urbangrnfrnds_01"), RAGE_JOAAT("urbanweeds01"), RAGE_JOAAT("prop_dandy_b"), RAGE_JOAAT("v_proc2_temp"), RAGE_JOAAT("prop_fernbb"), RAGE_JOAAT("proc_drygrassfronds01"), RAGE_JOAAT("prop_log_ae"), RAGE_JOAAT("prop_grass_da")};
constexpr uint32_t valid_player_models[] = {
RAGE_JOAAT("mp_m_freemode_01"),
@ -651,7 +651,7 @@ namespace big
if (!model_info::get_model(model))
return false;
if (!model_info::is_model_of_type(model, eModelType::Object, eModelType::Time, eModelType::Weapon, eModelType::Destructable, eModelType::WorldObject, eModelType::Sprinkler, eModelType::Unk65, eModelType::Plant, eModelType::LOD, eModelType::Unk132, eModelType::Building))
if (!model_info::is_model_of_type(model, eModelType::Object, eModelType::Time, eModelType::Weapon, eModelType::Destructable, eModelType::WorldObject, eModelType::Sprinkler, eModelType::Unk65, eModelType::LOD, eModelType::Unk132, eModelType::Building))
return true;
for (auto iterator : crash_objects)

View File

@ -5,6 +5,7 @@
#include "services/players/player_service.hpp"
#include <network/Network.hpp>
#include <network/netConnection.hpp>
namespace big
{
@ -27,7 +28,7 @@ namespace big
void packet::send(int peer_id, int connection_id)
{
auto mgr = gta_util::get_network()->m_game_session_ptr->m_net_connection_mgr;
auto peer = g_pointers->m_gta.m_get_peer_address(mgr, peer_id);
g_pointers->m_gta.m_send_packet(mgr, peer, connection_id, m_data, (m_buffer.m_curBit + 7) >> 3, 0x1000000);
auto peer = g_pointers->m_gta.m_get_connection_peer(mgr, peer_id);
g_pointers->m_gta.m_send_packet(mgr, &peer->m_peer_address, connection_id, m_data, (m_buffer.m_curBit + 7) >> 3, 0x1000000);
}
}

View File

@ -754,16 +754,24 @@ namespace big
g_pointers->m_gta.m_request_control = ptr.add(1).rip().as<functions::request_control>();
}
},
// Get Connection Peer & Send Remove Gamer Command
// Send Remove Gamer Command
{
"GCP&SRGC",
"SRGC",
"8D 42 FF 83 F8 FD 77 3D",
[](memory::handle ptr)
{
g_pointers->m_gta.m_get_peer_address = ptr.add(23).rip().as<functions::get_peer_address>();
g_pointers->m_gta.m_send_remove_gamer_cmd = ptr.add(65).rip().as<functions::send_remove_gamer_cmd>();
}
},
// Get Connection Peer
{
"GCP",
"48 89 5C 24 08 48 89 74 24 18 89 54 24 10 57 48 83 EC 40 48",
[](memory::handle ptr)
{
g_pointers->m_gta.m_get_connection_peer = ptr.as<functions::get_connection_peer>();
}
},
// Handle Remove Gamer Command
{
"HRGC",
@ -1282,6 +1290,15 @@ namespace big
g_pointers->m_gta.m_render_big_ped = ptr.as<PVOID*>();
}
},
// Force Relay Connections
{
"FRC",
"8A 05 ? ? ? ? 88 83 BC 00 00 00",
[](memory::handle ptr)
{
g_pointers->m_gta.m_force_relay_connections = ptr.add(2).rip().as<bool*>();
}
},
// Max Wanted Level
{
"MWL",

View File

@ -83,17 +83,30 @@ namespace big
return nullptr;
}
netAddress player::get_ip_address()
rage::netConnectionPeer* player::get_connection_peer()
{
if (auto session_player = get_session_player())
if (auto peer = g_pointers->m_gta.m_get_connection_peer(gta_util::get_network()->m_game_session_ptr->m_net_connection_mgr,
(int)get_session_player()->m_player_data.m_peer_id_2))
return peer;
return nullptr;
}
std::optional<netAddress> player::get_ip_address()
{
if (this == g_player_service->get_self().get() && get_net_data())
return get_net_data()->m_external_ip;
if (auto session_player = get_session_player())
if (auto peer = g_pointers->m_gta.m_get_peer_address(gta_util::get_network()->m_game_session_ptr->m_net_connection_mgr,
(int)get_session_player()->m_player_data.m_peer_id_2))
return netAddress{((rage::netPeerAddress*)peer)->m_external_ip};
if (auto peer = get_connection_peer())
{
if (peer->m_peer_address.m_connection_type != 1)
return std::nullopt;
return {0};
return peer->m_peer_address.m_external_ip;
}
return std::nullopt;
}
uint16_t player::get_port()
@ -101,10 +114,13 @@ namespace big
if (this == g_player_service->get_self().get() && get_net_data())
return get_net_data()->m_external_port;
if (auto session_player = get_session_player())
if (auto peer = g_pointers->m_gta.m_get_peer_address(gta_util::get_network()->m_game_session_ptr->m_net_connection_mgr,
(int)get_session_player()->m_player_data.m_peer_id_2))
return ((rage::netPeerAddress*)peer)->m_external_port;
if (auto peer = get_connection_peer())
{
if (peer->m_peer_address.m_connection_type != 1)
return 0;
return peer->m_peer_address.m_external_port;
}
return 0;
}

View File

@ -12,6 +12,7 @@ namespace rage
class snPlayer;
class snPeer;
class rlGamerInfo;
class netConnectionPeer;
}
namespace big
@ -44,7 +45,8 @@ namespace big
[[nodiscard]] CPlayerInfo* get_player_info() const;
[[nodiscard]] class rage::snPlayer* get_session_player();
[[nodiscard]] class rage::snPeer* get_session_peer();
[[nodiscard]] netAddress get_ip_address();
[[nodiscard]] class rage::netConnectionPeer* get_connection_peer();
[[nodiscard]] std::optional<netAddress> get_ip_address();
[[nodiscard]] uint16_t get_port();
[[nodiscard]] uint8_t id() const;

View File

@ -72,16 +72,17 @@ namespace big::spam
inline void log_chat(char* msg, player_ptr player, bool is_spam)
{
std::ofstream spam_log(g_file_manager.get_project_file(is_spam ? "./spam.log" : "./chat.log").get_path(), std::ios::app);
std::ofstream log(g_file_manager.get_project_file(is_spam ? "./spam.log" : "./chat.log").get_path(), std::ios::app);
auto& plData = *player->get_net_data();
auto ip = player->get_ip_address();
auto& data = *player->get_net_data();
auto ip = player->get_ip_address();
spam_log << player->get_name() << " (" << plData.m_gamer_handle.m_rockstar_id << ") <" << (int)ip.m_field1 << "."
<< (int)ip.m_field2 << "." << (int)ip.m_field3 << "." << (int)ip.m_field4 << ">: " << msg << std::endl;
if (ip)
log << player->get_name() << " (" << data.m_gamer_handle.m_rockstar_id << ") <" << (int)ip.value().m_field1 << "."
<< (int)ip.value().m_field2 << "." << (int)ip.value().m_field3 << "." << (int)ip.value().m_field4 << ">: " << msg << std::endl;
else
log << player->get_name() << " (" << data.m_gamer_handle.m_rockstar_id << ") <UNKNOWN>: " << msg << std::endl;
spam_log.close();
log.close();
}
}

View File

@ -10,6 +10,7 @@
#include "util/system.hpp"
#include <network/Network.hpp>
#include <network/netConnection.hpp>
#include <network/netTime.hpp>
#include <script/globals/GPBD_FM_3.hpp>
#include <timeapi.h>
@ -83,13 +84,15 @@ namespace big::toxic
.count();
msg.increment = millis;
auto peer = g_pointers->m_gta.m_get_peer_address(gta_util::get_network()->m_game_session_ptr->m_net_connection_mgr,
(int)target->get_session_player()->m_player_data.m_peer_id_2);
auto peer = target->get_connection_peer();
if (!peer)
return false;
for (int j = 0; j < 100; j++)
{
g_pointers->m_gta.m_sync_network_time(gta_util::get_network()->m_game_session_ptr->m_net_connection_mgr,
peer,
&peer->m_peer_address,
(*g_pointers->m_gta.m_network_time)->m_connection_identifier,
&msg,
0x1000000); // repeatedly spamming the event will eventually cause certain bounds checks to disable for some reason
@ -149,13 +152,15 @@ namespace big::toxic
.count();
msg.increment = millis;
auto peer = g_pointers->m_gta.m_get_peer_address(gta_util::get_network()->m_game_session_ptr->m_net_connection_mgr,
(int)plyr.second->get_session_player()->m_player_data.m_peer_id_2);
auto peer = plyr.second->get_connection_peer();
if (!peer)
return;
for (int j = 0; j < 25; j++)
{
g_pointers->m_gta.m_sync_network_time(gta_util::get_network()->m_game_session_ptr->m_net_connection_mgr,
peer,
&peer->m_peer_address,
(*g_pointers->m_gta.m_network_time)->m_connection_identifier,
&msg,
0x1000000);

View File

@ -5,12 +5,37 @@
#include "services/player_database/player_database_service.hpp"
#include "views/view.hpp"
#include <network/netConnection.hpp>
#include <script/globals/GPBD_FM.hpp>
#include <script/globals/GPBD_FM_3.hpp>
#include <script/globals/GlobalPlayerBD.hpp>
namespace big
{
const char* get_nat_type_str(int type)
{
switch (type)
{
case 1: return "Open";
case 2: return "Moderate";
case 3: return "Strict";
}
return "Unknown";
}
const char* get_connection_type_str(int type)
{
switch (type)
{
case 1: return "Direct";
case 2: return "Relay";
case 3: return "Peer Relay";
}
return "Unknown";
}
void view::player_info()
{
ImGui::BeginGroup();
@ -36,6 +61,8 @@ namespace big
components::options_modal(
"Extra Info",
[ped_health, ped_maxhealth] {
ImGui::BeginGroup();
auto id = g_player_service->get_selected()->id();
if (id != -1)
@ -65,9 +92,41 @@ namespace big
ImGui::Text("PLAYER_INFO_METLDOWN_COMPLETE"_T.data(),
scr_globals::gpbd_fm_1.as<GPBD_FM*>()->Entries[id].MeltdownComplete ? "YES"_T.data() :
"NO"_T.data()); // curious to see if anyone has actually played singleplayer
ImGui::Separator();
}
ImGui::EndGroup();
ImGui::SameLine();
ImGui::BeginGroup();
ImGui::Text("NAT Type: %s", get_nat_type_str(g_player_service->get_selected()->get_net_data()->m_nat_type));
if (auto peer = g_player_service->get_selected()->get_connection_peer())
{
ImGui::Text("Connection Type: %s", get_connection_type_str(peer->m_peer_address.m_connection_type));
if (peer->m_peer_address.m_connection_type == 2)
{
auto ip = peer->m_relay_address.m_relay_address;
ImGui::Text("Relay IP: %d.%d.%d.%d", ip.m_field1, ip.m_field2, ip.m_field3, ip.m_field4);
}
else if (peer->m_peer_address.m_connection_type == 3)
{
auto ip = peer->m_peer_address.m_relay_address;
ImGui::Text("Peer Relay IP: %d.%d.%d.%d", ip.m_field1, ip.m_field2, ip.m_field3, ip.m_field4);
}
ImGui::Text("Num Messages Sent: %d", peer->m_num_messages_batched);
ImGui::Text("Num Reliables Sent: %d", peer->m_num_reliable_messages_batched);
ImGui::Text("Num Reliables Resent: %d", peer->m_num_resent_reliable_messages_batched);
ImGui::Text("Num Encryption Attempts: %d", peer->m_num_encryption_attempts);
}
ImGui::EndGroup();
ImGui::Separator();
ImGui::Checkbox("Block Explosions", &g_player_service->get_selected()->block_explosions);
ImGui::Checkbox("Block Clone Creates", &g_player_service->get_selected()->block_clone_create);
ImGui::Checkbox("Block Clone Syncs", &g_player_service->get_selected()->block_clone_sync);
@ -201,15 +260,47 @@ namespace big
auto ip = g_player_service->get_selected()->get_ip_address();
auto port = g_player_service->get_selected()->get_port();
ImGui::Text("PLAYER_INFO_IP"_T.data(), ip.m_field1, ip.m_field2, ip.m_field3, ip.m_field4, port);
if (ip)
{
ImGui::Text("PLAYER_INFO_IP"_T.data(),
ip.value().m_field1,
ip.value().m_field2,
ip.value().m_field3,
ip.value().m_field4,
port);
ImGui::SameLine();
ImGui::SameLine();
ImGui::PushID("##ip");
if (ImGui::SmallButton("COPY"_T.data()))
ImGui::SetClipboardText(
std::format("{}.{}.{}.{}:{}", ip.m_field1, ip.m_field2, ip.m_field3, ip.m_field4, port).data());
ImGui::PopID();
// clang-format off
ImGui::PushID("##ip");
if (ImGui::SmallButton("COPY"_T.data()))
ImGui::SetClipboardText(std::format("{}.{}.{}.{}:{}",
ip.value().m_field1,
ip.value().m_field2,
ip.value().m_field3,
ip.value().m_field4,
port).data());
ImGui::PopID();
// clang-format on
}
else
{
if (net_player_data->m_force_relays)
ImGui::Text("IP Address: Hidden");
else
ImGui::Text("IP Address: Unknown");
auto cxn_type = g_player_service->get_selected()->get_connection_peer() ?
g_player_service->get_selected()->get_connection_peer()->m_peer_address.m_connection_type :
0;
if (g.protections.force_relay_connections && ImGui::IsItemHovered())
ImGui::SetTooltip("IP addresses cannot be seen when Force Relay Connections is enabled");
else if (cxn_type == 2 && ImGui::IsItemHovered())
ImGui::SetTooltip("Cannot retrieve IP address since this player is connected through dedicated servers");
else if (cxn_type == 3 && ImGui::IsItemHovered())
ImGui::SetTooltip("Cannot retrieve IP address since this player is connected through another player");
}
}
ImGui::EndListBox();

View File

@ -57,6 +57,7 @@ namespace big
ImGui::SetTooltip("This prevents the collection of pickups such as unwanted money bags\nNote: Normal pickups are also no longer possible to collect with this enabled");
ImGui::Checkbox("ADMIN_CHECK"_T.data(), &g.protections.admin_check);
ImGui::Checkbox("Kick Rejoin", &g.protections.kick_rejoin);
components::command_checkbox<"forcerelays">();
ImGui::EndGroup();
ImGui::SeparatorText("Options");