mirror of
https://github.com/XLabsProject/s1x-client.git
synced 2023-08-02 15:02:12 +02:00
Merge pull request #167 from XLabsProject/release/v1.0.3
Release v1.0.3
This commit is contained in:
commit
8ecc8f9e34
7
.github/dependabot.yml
vendored
Normal file
7
.github/dependabot.yml
vendored
Normal file
@ -0,0 +1,7 @@
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: gitsubmodule
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: daily
|
||||
open-pull-requests-limit: 10
|
2
.gitmodules
vendored
2
.gitmodules
vendored
@ -10,7 +10,7 @@
|
||||
[submodule "deps/protobuf"]
|
||||
path = deps/protobuf
|
||||
url = https://github.com/google/protobuf.git
|
||||
branch = 3.14.x
|
||||
branch = 3.15.x
|
||||
[submodule "deps/minhook"]
|
||||
path = deps/minhook
|
||||
url = https://github.com/TsudaKageyu/minhook.git
|
||||
|
19
CHANGELOG.md
19
CHANGELOG.md
@ -7,6 +7,21 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## [v1.0.3] - 2021-05-04
|
||||
|
||||
### Added
|
||||
|
||||
- Use patrons for bot names [#128](https://github.com/XLabsProject/s1x-client/issues/128)
|
||||
- Increase network packet limit [#102](https://github.com/XLabsProject/s1x-client/issues/102)
|
||||
|
||||
### Fixed
|
||||
|
||||
- Possible duplicate GUID [#166](https://github.com/XLabsProject/s1x-client/issues/166)
|
||||
|
||||
### Pull Requests
|
||||
|
||||
- Game variable support #161 [#164](https://github.com/XLabsProject/s1x-client/pull/164) ([@fedddddd](https://github.com/fedddddd))
|
||||
|
||||
## [v1.0.2] - 2021-04-29
|
||||
|
||||
### Added
|
||||
@ -111,7 +126,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
- Discord RPC - party size + party size max [#59](https://github.com/XLabsProject/s1x-client/pull/59) ([@mjkzy](https://github.com/mjkzy))
|
||||
- discord presence - host name address [#64](https://github.com/XLabsProject/s1x-client/pull/64) ([@mjkzy](https://github.com/mjkzy))
|
||||
|
||||
[Unreleased]: https://github.com/XLabsProject/s1x-client/compare/v1.0.2...HEAD
|
||||
[Unreleased]: https://github.com/XLabsProject/s1x-client/compare/v1.0.3...HEAD
|
||||
|
||||
[v1.0.3]: https://github.com/XLabsProject/s1x-client/compare/v1.0.2...v1.0.3
|
||||
|
||||
[v1.0.2]: https://github.com/XLabsProject/s1x-client/compare/v1.0.1...v1.0.2
|
||||
|
||||
|
2
deps/protobuf
vendored
2
deps/protobuf
vendored
@ -1 +1 @@
|
||||
Subproject commit 19fb89416f3fdc2d6668f3738f444885575285bc
|
||||
Subproject commit 436bd7880e458532901c58f4d9d1ea23fa7edd52
|
@ -17,16 +17,62 @@ namespace auth
|
||||
{
|
||||
namespace
|
||||
{
|
||||
std::string get_key_entropy()
|
||||
std::string get_hdd_serial()
|
||||
{
|
||||
auto uuid = utils::smbios::get_uuid();
|
||||
if (uuid.empty())
|
||||
DWORD serial{};
|
||||
if (!GetVolumeInformationA("C:\\", nullptr, 0, &serial, nullptr, nullptr, nullptr, 0))
|
||||
{
|
||||
uuid.resize(16);
|
||||
utils::cryptography::random::get_data(uuid.data(), uuid.size());
|
||||
return {};
|
||||
}
|
||||
|
||||
return uuid;
|
||||
return utils::string::va("%08X", serial);
|
||||
}
|
||||
|
||||
std::string get_hw_profile_guid()
|
||||
{
|
||||
HW_PROFILE_INFO info;
|
||||
if (!GetCurrentHwProfileA(&info))
|
||||
{
|
||||
return {};
|
||||
}
|
||||
|
||||
return std::string{info.szHwProfileGuid, sizeof(info.szHwProfileGuid)};
|
||||
}
|
||||
|
||||
std::string get_protected_data()
|
||||
{
|
||||
std::string input = "X-Labs-S1x-Auth";
|
||||
|
||||
DATA_BLOB data_in{}, data_out{};
|
||||
data_in.pbData = reinterpret_cast<uint8_t*>(input.data());
|
||||
data_in.cbData = static_cast<DWORD>(input.size());
|
||||
if(CryptProtectData(&data_in, nullptr, nullptr, nullptr, nullptr, CRYPTPROTECT_LOCAL_MACHINE, &data_out) != TRUE)
|
||||
{
|
||||
return {};
|
||||
}
|
||||
|
||||
const auto size = std::min(data_out.cbData, 52ul);
|
||||
std::string result{reinterpret_cast<char*>(data_out.pbData), size};
|
||||
LocalFree(data_out.pbData);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
std::string get_key_entropy()
|
||||
{
|
||||
std::string entropy{};
|
||||
entropy.append(utils::smbios::get_uuid());
|
||||
entropy.append(get_hw_profile_guid());
|
||||
entropy.append(get_protected_data());
|
||||
entropy.append(get_hdd_serial());
|
||||
|
||||
if (entropy.empty())
|
||||
{
|
||||
entropy.resize(32);
|
||||
utils::cryptography::random::get_data(entropy.data(), entropy.size());
|
||||
}
|
||||
|
||||
return entropy;
|
||||
}
|
||||
|
||||
utils::cryptography::ecc::key& get_key()
|
||||
|
@ -132,6 +132,25 @@ namespace dedicated
|
||||
|
||||
com_quit_f_hook.invoke<void>();
|
||||
}
|
||||
|
||||
void sys_error_stub(const char* msg, ...)
|
||||
{
|
||||
char buffer[2048];
|
||||
|
||||
va_list ap;
|
||||
va_start(ap, msg);
|
||||
|
||||
vsnprintf_s(buffer, sizeof(buffer), _TRUNCATE, msg, ap);
|
||||
|
||||
va_end(ap);
|
||||
|
||||
scheduler::once([]()
|
||||
{
|
||||
command::execute("map_rotate");
|
||||
}, scheduler::main, 3s);
|
||||
|
||||
game::Com_Error(game::ERR_DROP, "%s", buffer);
|
||||
}
|
||||
}
|
||||
|
||||
void initialize()
|
||||
@ -171,6 +190,9 @@ namespace dedicated
|
||||
// Don't allow sv_hostname to be changed by the game
|
||||
dvars::disable::Dvar_SetString("sv_hostname");
|
||||
|
||||
// Stop crashing from sys_errors
|
||||
utils::hook::jump(0x1404D6260, sys_error_stub);
|
||||
|
||||
// Hook R_SyncGpu
|
||||
utils::hook::jump(0x1405A7630, sync_gpu_stub);
|
||||
|
||||
|
@ -62,79 +62,83 @@ namespace logfile
|
||||
}
|
||||
|
||||
void scr_player_killed_stub(game::mp::gentity_s* self, const game::mp::gentity_s* inflictor, game::mp::gentity_s* attacker, int damage,
|
||||
int meansOfDeath, const unsigned int weapon, bool isAlternate, const float* vDir, const unsigned int hitLoc, int psTimeOffset, int deathAnimDuration)
|
||||
const int meansOfDeath, const unsigned int weapon, const bool isAlternate, const float* vDir, const unsigned int hitLoc, int psTimeOffset, int deathAnimDuration)
|
||||
{
|
||||
const std::string _hitLoc = reinterpret_cast<const char**>(0x1409B5400)[hitLoc];
|
||||
const auto _mod = convert_mod(meansOfDeath);
|
||||
|
||||
const auto _weapon = get_weapon_name(weapon, isAlternate);
|
||||
|
||||
for (const auto& callback : player_killed_callbacks)
|
||||
{
|
||||
const auto state = callback.lua_state();
|
||||
const std::string _hitLoc = reinterpret_cast<const char**>(0x1409B5400)[hitLoc];
|
||||
const auto _mod = convert_mod(meansOfDeath);
|
||||
|
||||
const auto _self = convert_entity(state, self);
|
||||
const auto _inflictor = convert_entity(state, inflictor);
|
||||
const auto _attacker = convert_entity(state, attacker);
|
||||
const auto _weapon = get_weapon_name(weapon, isAlternate);
|
||||
|
||||
const auto _vDir = convert_vector(state, vDir);
|
||||
|
||||
const auto result = callback(_self, _inflictor, _attacker, damage, _mod, _weapon, _vDir, _hitLoc, psTimeOffset, deathAnimDuration);
|
||||
|
||||
scripting::lua::handle_error(result);
|
||||
|
||||
if (result.valid() && result.get_type() == sol::type::number)
|
||||
for (const auto& callback : player_killed_callbacks)
|
||||
{
|
||||
damage = result.get<int>();
|
||||
}
|
||||
}
|
||||
const auto state = callback.lua_state();
|
||||
|
||||
if (damage == 0)
|
||||
{
|
||||
return;
|
||||
const auto _self = convert_entity(state, self);
|
||||
const auto _inflictor = convert_entity(state, inflictor);
|
||||
const auto _attacker = convert_entity(state, attacker);
|
||||
|
||||
const auto _vDir = convert_vector(state, vDir);
|
||||
|
||||
const auto result = callback(_self, _inflictor, _attacker, damage, _mod, _weapon, _vDir, _hitLoc, psTimeOffset, deathAnimDuration);
|
||||
|
||||
scripting::lua::handle_error(result);
|
||||
|
||||
if (result.valid() && result.get_type() == sol::type::number)
|
||||
{
|
||||
damage = result.get<int>();
|
||||
}
|
||||
}
|
||||
|
||||
if (damage == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
scr_player_killed_hook.invoke<void>(self, inflictor, attacker, damage, meansOfDeath, weapon, isAlternate, vDir, hitLoc, psTimeOffset, deathAnimDuration);
|
||||
}
|
||||
|
||||
void scr_player_damage_stub(game::mp::gentity_s* self, const game::mp::gentity_s* inflictor, game::mp::gentity_s* attacker, int damage, int dflags,
|
||||
int meansOfDeath, const unsigned int weapon, bool isAlternate, const float* vPoint, const float* vDir, const unsigned int hitLoc, int timeOffset)
|
||||
const int meansOfDeath, const unsigned int weapon, const bool isAlternate, const float* vPoint, const float* vDir, const unsigned int hitLoc, const int timeOffset)
|
||||
{
|
||||
const std::string _hitLoc = reinterpret_cast<const char**>(0x1409B5400)[hitLoc];
|
||||
const auto _mod = convert_mod(meansOfDeath);
|
||||
|
||||
const auto _weapon = get_weapon_name(weapon, isAlternate);
|
||||
|
||||
for (const auto& callback : player_damage_callbacks)
|
||||
{
|
||||
const auto state = callback.lua_state();
|
||||
const std::string _hitLoc = reinterpret_cast<const char**>(0x1409B5400)[hitLoc];
|
||||
const auto _mod = convert_mod(meansOfDeath);
|
||||
|
||||
const auto _self = convert_entity(state, self);
|
||||
const auto _inflictor = convert_entity(state, inflictor);
|
||||
const auto _attacker = convert_entity(state, attacker);
|
||||
const auto _weapon = get_weapon_name(weapon, isAlternate);
|
||||
|
||||
const auto _vPoint = convert_vector(state, vPoint);
|
||||
const auto _vDir = convert_vector(state, vDir);
|
||||
|
||||
const auto result = callback(_self, _inflictor, _attacker, damage, dflags, _mod, _weapon, _vPoint, _vDir, _hitLoc);
|
||||
|
||||
scripting::lua::handle_error(result);
|
||||
|
||||
if (result.valid() && result.get_type() == sol::type::number)
|
||||
for (const auto& callback : player_damage_callbacks)
|
||||
{
|
||||
damage = result.get<int>();
|
||||
}
|
||||
}
|
||||
const auto state = callback.lua_state();
|
||||
|
||||
if (damage == 0)
|
||||
{
|
||||
return;
|
||||
const auto _self = convert_entity(state, self);
|
||||
const auto _inflictor = convert_entity(state, inflictor);
|
||||
const auto _attacker = convert_entity(state, attacker);
|
||||
|
||||
const auto _vPoint = convert_vector(state, vPoint);
|
||||
const auto _vDir = convert_vector(state, vDir);
|
||||
|
||||
const auto result = callback(_self, _inflictor, _attacker, damage, dflags, _mod, _weapon, _vPoint, _vDir, _hitLoc);
|
||||
|
||||
scripting::lua::handle_error(result);
|
||||
|
||||
if (result.valid() && result.get_type() == sol::type::number)
|
||||
{
|
||||
damage = result.get<int>();
|
||||
}
|
||||
}
|
||||
|
||||
if (damage == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
scr_player_damage_hook.invoke<void>(self, inflictor, attacker, damage, dflags, meansOfDeath, weapon, isAlternate, vPoint, vDir, hitLoc, timeOffset);
|
||||
}
|
||||
|
||||
void client_command_stub(int clientNum)
|
||||
void client_command_stub(const int clientNum)
|
||||
{
|
||||
auto self = &game::mp::g_entities[clientNum];
|
||||
char cmd[1024]{};
|
||||
@ -168,10 +172,12 @@ namespace logfile
|
||||
return reinterpret_cast<void(*)(int)>(0x1402E98F0)(clientNum);
|
||||
}
|
||||
|
||||
void g_shutdown_game_stub(int freeScripts)
|
||||
void g_shutdown_game_stub(const int freeScripts)
|
||||
{
|
||||
const scripting::entity level{*game::levelEntityId};
|
||||
scripting::notify(level, "shutdownGame_called", {1});
|
||||
{
|
||||
const scripting::entity level{*game::levelEntityId};
|
||||
scripting::notify(level, "shutdownGame_called", {1});
|
||||
}
|
||||
|
||||
// G_ShutdownGame
|
||||
return reinterpret_cast<void(*)(int)>(0x1402F8C10)(freeScripts);
|
||||
|
@ -10,6 +10,8 @@ namespace logger
|
||||
{
|
||||
namespace
|
||||
{
|
||||
utils::hook::detour com_error_hook;
|
||||
|
||||
void print_error(const char* msg, ...)
|
||||
{
|
||||
char buffer[2048];
|
||||
@ -38,6 +40,24 @@ namespace logger
|
||||
console::error(buffer);
|
||||
}
|
||||
|
||||
void com_error_stub(const int error, const char* msg, ...)
|
||||
{
|
||||
char buffer[2048];
|
||||
|
||||
{
|
||||
va_list ap;
|
||||
va_start(ap, msg);
|
||||
|
||||
vsnprintf_s(buffer, sizeof(buffer), _TRUNCATE, msg, ap);
|
||||
|
||||
va_end(ap);
|
||||
|
||||
console::error(buffer);
|
||||
}
|
||||
|
||||
com_error_hook.invoke<void>(error, "%s", buffer);
|
||||
}
|
||||
|
||||
void print_warning(const char* msg, ...)
|
||||
{
|
||||
char buffer[2048];
|
||||
@ -145,6 +165,7 @@ namespace logger
|
||||
sub_1400E7420();
|
||||
|
||||
utils::hook::call(0x1404D8543, print_com_error);
|
||||
com_error_hook.create(0x1403CE480, com_error_stub);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
@ -59,10 +59,7 @@ namespace scripting
|
||||
|
||||
void g_shutdown_game_stub(const int free_scripts)
|
||||
{
|
||||
if (!game::VirtualLobby_Loaded())
|
||||
{
|
||||
lua::engine::stop();
|
||||
}
|
||||
lua::engine::stop();
|
||||
return g_shutdown_game_hook.invoke<void>(free_scripts);
|
||||
}
|
||||
}
|
||||
|
@ -43,8 +43,8 @@ namespace server_list
|
||||
std::vector<server_info> servers;
|
||||
|
||||
size_t server_list_page = 0;
|
||||
|
||||
volatile bool update_server_list = false;
|
||||
std::chrono::high_resolution_clock::time_point last_scroll{};
|
||||
|
||||
size_t get_page_count()
|
||||
{
|
||||
@ -220,6 +220,11 @@ namespace server_list
|
||||
return game::Menu_IsMenuOpenAndVisible(0, "menu_systemlink_join");
|
||||
}
|
||||
|
||||
bool is_scrolling_disabled()
|
||||
{
|
||||
return update_server_list || (std::chrono::high_resolution_clock::now() - last_scroll) < 500ms;
|
||||
}
|
||||
|
||||
bool scroll_down()
|
||||
{
|
||||
if (!is_server_list_open())
|
||||
@ -227,8 +232,9 @@ namespace server_list
|
||||
return false;
|
||||
}
|
||||
|
||||
if (server_list_page + 1 < get_page_count())
|
||||
if (!is_scrolling_disabled() && server_list_page + 1 < get_page_count())
|
||||
{
|
||||
last_scroll = std::chrono::high_resolution_clock::now();
|
||||
++server_list_page;
|
||||
trigger_refresh();
|
||||
}
|
||||
@ -243,8 +249,9 @@ namespace server_list
|
||||
return false;
|
||||
}
|
||||
|
||||
if (server_list_page > 0)
|
||||
if (!is_scrolling_disabled() && server_list_page > 0)
|
||||
{
|
||||
last_scroll = std::chrono::high_resolution_clock::now();
|
||||
--server_list_page;
|
||||
trigger_refresh();
|
||||
}
|
||||
|
@ -259,6 +259,19 @@ namespace scripting::lua
|
||||
{
|
||||
logfile::add_player_killed_callback(callback);
|
||||
};
|
||||
|
||||
game_type["getgamevar"] = [](const sol::this_state s)
|
||||
{
|
||||
const auto id = *reinterpret_cast<unsigned int*>(0x14815DEB4);
|
||||
|
||||
const auto value = ::game::scr_VarGlob->childVariableValue[id];
|
||||
|
||||
::game::VariableValue variable{};
|
||||
variable.type = value.type;
|
||||
variable.u.uintValue = value.u.u.uintValue;
|
||||
|
||||
return convert(s, variable);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@ -301,7 +314,7 @@ namespace scripting::lua
|
||||
|
||||
context::~context()
|
||||
{
|
||||
this->state_.collect_garbage();
|
||||
this->collect_garbage();
|
||||
this->scheduler_.clear();
|
||||
this->event_handler_.clear();
|
||||
this->state_ = {};
|
||||
@ -310,7 +323,7 @@ namespace scripting::lua
|
||||
void context::run_frame()
|
||||
{
|
||||
this->scheduler_.run_frame();
|
||||
this->state_.collect_garbage();
|
||||
this->collect_garbage();
|
||||
}
|
||||
|
||||
void context::notify(const event& e)
|
||||
@ -318,6 +331,11 @@ namespace scripting::lua
|
||||
this->event_handler_.dispatch(e);
|
||||
}
|
||||
|
||||
void context::collect_garbage()
|
||||
{
|
||||
this->state_.collect_garbage();
|
||||
}
|
||||
|
||||
void context::load_script(const std::string& script)
|
||||
{
|
||||
if (!this->loaded_scripts_.emplace(script).second)
|
||||
|
@ -30,6 +30,7 @@ namespace scripting::lua
|
||||
|
||||
void run_frame();
|
||||
void notify(const event& e);
|
||||
void collect_garbage();
|
||||
|
||||
private:
|
||||
sol::state state_{};
|
||||
|
@ -32,12 +32,18 @@ namespace scripting::lua::engine
|
||||
{
|
||||
if (std::filesystem::is_directory(script) && utils::io::file_exists(script + "/__init__.lua"))
|
||||
{
|
||||
get_scripts().push_back(std::make_unique<context>(script));
|
||||
get_scripts().emplace_back(std::make_unique<context>(script));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void stop()
|
||||
{
|
||||
logfile::clear_callbacks();
|
||||
get_scripts().clear();
|
||||
}
|
||||
|
||||
void start()
|
||||
{
|
||||
// No SP until there is a concept
|
||||
@ -46,17 +52,10 @@ namespace scripting::lua::engine
|
||||
return;
|
||||
}
|
||||
|
||||
clear_custom_fields();
|
||||
get_scripts().clear();
|
||||
stop();
|
||||
load_scripts();
|
||||
}
|
||||
|
||||
void stop()
|
||||
{
|
||||
logfile::clear_callbacks();
|
||||
get_scripts().clear();
|
||||
}
|
||||
|
||||
void notify(const event& e)
|
||||
{
|
||||
for (auto& script : get_scripts())
|
||||
|
@ -18,19 +18,9 @@ namespace scripting::lua
|
||||
|
||||
void event_handler::dispatch(const event& event)
|
||||
{
|
||||
std::vector<sol::lua_value> arguments;
|
||||
bool has_built_arguments = false;
|
||||
event_arguments arguments{};
|
||||
|
||||
for (const auto& argument : event.arguments)
|
||||
{
|
||||
arguments.emplace_back(convert(this->state_, argument));
|
||||
}
|
||||
|
||||
this->dispatch_to_specific_listeners(event, arguments);
|
||||
}
|
||||
|
||||
void event_handler::dispatch_to_specific_listeners(const event& event,
|
||||
const event_arguments& arguments)
|
||||
{
|
||||
callbacks_.access([&](task_list& tasks)
|
||||
{
|
||||
this->merge_callbacks();
|
||||
@ -45,6 +35,12 @@ namespace scripting::lua
|
||||
|
||||
if (!i->is_deleted)
|
||||
{
|
||||
if(!has_built_arguments)
|
||||
{
|
||||
has_built_arguments = true;
|
||||
arguments = this->build_arguments(event);
|
||||
}
|
||||
|
||||
handle_error(i->callback(sol::as_args(arguments)));
|
||||
}
|
||||
|
||||
@ -116,4 +112,16 @@ namespace scripting::lua
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
event_arguments event_handler::build_arguments(const event& event) const
|
||||
{
|
||||
event_arguments arguments;
|
||||
|
||||
for (const auto& argument : event.arguments)
|
||||
{
|
||||
arguments.emplace_back(convert(this->state_, argument));
|
||||
}
|
||||
|
||||
return arguments;
|
||||
}
|
||||
}
|
||||
|
@ -46,9 +46,9 @@ namespace scripting::lua
|
||||
utils::concurrency::container<task_list> new_callbacks_;
|
||||
utils::concurrency::container<task_list, std::recursive_mutex> callbacks_;
|
||||
|
||||
void dispatch_to_specific_listeners(const event& event, const event_arguments& arguments);
|
||||
|
||||
void remove(const event_listener_handle& handle);
|
||||
void merge_callbacks();
|
||||
|
||||
event_arguments build_arguments(const event& event) const;
|
||||
};
|
||||
}
|
||||
|
@ -44,6 +44,7 @@
|
||||
#include <urlmon.h>
|
||||
#include <atlbase.h>
|
||||
#include <iphlpapi.h>
|
||||
#include <wincrypt.h>
|
||||
|
||||
// min and max is required by gdi, therefore NOMINMAX won't work
|
||||
#ifdef max
|
||||
@ -84,6 +85,7 @@
|
||||
#include <asmjit/core/jitruntime.h>
|
||||
#include <asmjit/x86/x86assembler.h>
|
||||
|
||||
#include <google/protobuf/stubs/logging.h>
|
||||
#include <proto/auth.pb.h>
|
||||
|
||||
#pragma warning(pop)
|
||||
@ -93,6 +95,7 @@
|
||||
#pragma comment(lib, "ws2_32.lib")
|
||||
#pragma comment(lib, "urlmon.lib" )
|
||||
#pragma comment(lib, "iphlpapi.lib")
|
||||
#pragma comment(lib, "Crypt32.lib")
|
||||
|
||||
#include "resource.hpp"
|
||||
|
||||
|
Binary file not shown.
BIN
tools/protoc.exe
BIN
tools/protoc.exe
Binary file not shown.
Loading…
Reference in New Issue
Block a user