More LUI scripting support
This commit is contained in:
parent
401006ca8e
commit
2b6989692f
@ -36,6 +36,12 @@ namespace lui
|
||||
|
||||
game::LUI_OpenMenu(0, params[1], 1, 0, 0);
|
||||
});
|
||||
|
||||
command::add("lui_restart", []()
|
||||
{
|
||||
utils::hook::invoke<void>(game::base_address + 0x3203B0);
|
||||
utils::hook::invoke<void>(game::base_address + 0x32D370);
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
|
@ -21,19 +21,20 @@
|
||||
|
||||
namespace ui_scripting
|
||||
{
|
||||
std::unordered_map<std::string, game::hks::lua_function> functions;
|
||||
std::unordered_map<std::string, game::hks::lua_function> methods;
|
||||
|
||||
namespace
|
||||
{
|
||||
utils::hook::detour hksi_open_lib_hook;
|
||||
utils::hook::detour hksi_lual_error_hook;
|
||||
utils::hook::detour hksi_lual_error_hook2;
|
||||
utils::hook::detour hksi_add_function_hook;
|
||||
utils::hook::detour hks_start_hook;
|
||||
utils::hook::detour hks_shutdown_hook;
|
||||
|
||||
bool error_hook_enabled = false;
|
||||
|
||||
std::unordered_map<std::string, game::hks::lua_function> functions;
|
||||
std::unordered_map<std::string, game::hks::lua_function> methods;
|
||||
|
||||
void* hksi_open_lib_stub(game::hks::lua_State* s, const char* libname, game::hks::luaL_Reg* l)
|
||||
{
|
||||
for (auto i = l; i->name; ++i)
|
||||
@ -184,6 +185,7 @@ namespace ui_scripting
|
||||
hks_shutdown_hook.create(game::base_address + 0x3203B0, hks_shutdown_stub);
|
||||
hksi_open_lib_hook.create(game::base_address + 0x2E4530, hksi_open_lib_stub);
|
||||
hksi_lual_error_hook.create(game::base_address + 0x2E3E40, hksi_lual_error_stub);
|
||||
hksi_lual_error_hook2.create(game::base_address + 0x2DCB40, hksi_lual_error_stub);
|
||||
hksi_add_function_hook.create(game::base_address + 0x2DB570, hksi_add_function_stub);
|
||||
}
|
||||
};
|
||||
|
@ -4,6 +4,9 @@
|
||||
|
||||
namespace ui_scripting
|
||||
{
|
||||
extern std::unordered_map<std::string, game::hks::lua_function> functions;
|
||||
extern std::unordered_map<std::string, game::hks::lua_function> methods;
|
||||
|
||||
void enable_error_hook();
|
||||
void disable_error_hook();
|
||||
|
||||
|
@ -942,12 +942,14 @@ namespace game
|
||||
char m_data[30];
|
||||
};
|
||||
|
||||
struct HashTable;
|
||||
|
||||
union HksValue
|
||||
{
|
||||
void* cClosure;
|
||||
void* closure;
|
||||
UserData* userData;
|
||||
void* table;
|
||||
HashTable* table;
|
||||
void* tstruct;
|
||||
InternString* str;
|
||||
void* thread;
|
||||
@ -983,15 +985,31 @@ namespace game
|
||||
HksValue v;
|
||||
};
|
||||
|
||||
struct lua_State
|
||||
struct CallStack
|
||||
{
|
||||
void* m_records;
|
||||
void* m_lastrecord;
|
||||
void* m_current;
|
||||
const unsigned int* m_current_lua_pc;
|
||||
const unsigned int* m_hook_return_addr;
|
||||
int m_hook_level;
|
||||
};
|
||||
|
||||
struct ApiStack
|
||||
{
|
||||
char __pad0[72];
|
||||
HksObject* top;
|
||||
HksObject* base;
|
||||
HksObject* alloc_top;
|
||||
HksObject* bottom;
|
||||
};
|
||||
|
||||
struct lua_State
|
||||
{
|
||||
char __pad0[24];
|
||||
CallStack m_callStack;
|
||||
ApiStack m_apistack;
|
||||
};
|
||||
|
||||
using lua_function = int(__fastcall*)(lua_State*);
|
||||
|
||||
struct luaL_Reg
|
||||
@ -999,5 +1017,26 @@ namespace game
|
||||
const char* name;
|
||||
lua_function function;
|
||||
};
|
||||
|
||||
struct Node
|
||||
{
|
||||
HksObject m_key;
|
||||
HksObject m_value;
|
||||
};
|
||||
|
||||
struct Metatable
|
||||
{
|
||||
};
|
||||
|
||||
struct HashTable : ChunkHeader
|
||||
{
|
||||
Metatable* m_meta;
|
||||
unsigned int m_version;
|
||||
unsigned int m_mask;
|
||||
Node* m_hashPart;
|
||||
HksObject* m_arrayPart;
|
||||
unsigned int m_arraySize;
|
||||
Node* m_freeNode;
|
||||
};
|
||||
}
|
||||
}
|
@ -155,5 +155,10 @@ namespace game
|
||||
WEAK symbol<const char*(lua_State* s, const HksObject* obj, unsigned int* len)> hks_obj_tolstring{0x287410};
|
||||
WEAK symbol<int(lua_State* s, const HksObject* obj, HksObject* ret)> hks_obj_getmetatable{0x2DA210};
|
||||
WEAK symbol<HksObject*(HksObject* result, lua_State* s, const HksObject* table, const HksObject* key)> hks_obj_getfield{0x2D9E20};
|
||||
WEAK symbol<void(lua_State* s, const HksObject* tbl, const HksObject* key, const HksObject* val)> hks_obj_settable{0x2DB040};
|
||||
WEAK symbol<void(lua_State* s, int nargs, int nresults, const unsigned int* pc)> vm_call_internal{0x30AB60};
|
||||
WEAK symbol<void(lua_State* s, int index)> hksi_lua_pushvalue{0x2DE040};
|
||||
WEAK symbol<HashTable*(lua_State* s, unsigned int arraySize, unsigned int hashSize)> Hashtable_Create{0x2C8290};
|
||||
WEAK symbol<HksObject*(HashTable* t, HksObject* result, HksObject* key)> Hashtable_getNextHash{0x2D5150};
|
||||
}
|
||||
}
|
@ -1,5 +1,4 @@
|
||||
#include <std_include.hpp>
|
||||
#include "value.hpp"
|
||||
#include "execution.hpp"
|
||||
#include "stack_isolation.hpp"
|
||||
#include "component/ui_scripting.hpp"
|
||||
@ -8,122 +7,206 @@
|
||||
|
||||
namespace ui_scripting
|
||||
{
|
||||
namespace
|
||||
void push_value(const value& value)
|
||||
{
|
||||
bool valid_state()
|
||||
{
|
||||
return *game::hks::lua_state != 0;
|
||||
}
|
||||
|
||||
void push_value(const value& value_)
|
||||
{
|
||||
if (!valid_state())
|
||||
{
|
||||
throw std::runtime_error("Invalid lua state");
|
||||
}
|
||||
|
||||
const auto state = *game::hks::lua_state;
|
||||
|
||||
switch (value_.index())
|
||||
{
|
||||
case 1:
|
||||
{
|
||||
const auto value = std::get<bool>(value_);
|
||||
state->top->t = game::hks::HksObjectType::TBOOLEAN;
|
||||
state->top->v.boolean = value;
|
||||
state->top++;
|
||||
break;
|
||||
}
|
||||
case 2:
|
||||
{
|
||||
const auto value = std::get<int>(value_);
|
||||
state->top->t = game::hks::HksObjectType::TNUMBER;
|
||||
state->top->v.number = static_cast<float>(value);
|
||||
state->top++;
|
||||
break;
|
||||
}
|
||||
case 3:
|
||||
{
|
||||
const auto value = std::get<float>(value_);
|
||||
state->top->t = game::hks::HksObjectType::TNUMBER;
|
||||
state->top->v.number = value;
|
||||
state->top++;
|
||||
break;
|
||||
}
|
||||
case 4:
|
||||
{
|
||||
const auto str = std::get<std::string>(value_);
|
||||
game::hks::hksi_lua_pushlstring(state, str.data(), (unsigned int)str.size());
|
||||
break;
|
||||
}
|
||||
case 5:
|
||||
{
|
||||
const auto data = std::get<lightuserdata>(value_);
|
||||
state->top->t = game::hks::HksObjectType::TLIGHTUSERDATA;
|
||||
state->top->v.ptr = data.ptr;
|
||||
state->top++;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool is_integer(float number)
|
||||
{
|
||||
return static_cast<int>(number) == number;
|
||||
const auto value_ = value.get_raw();
|
||||
*state->m_apistack.top = value_;
|
||||
state->m_apistack.top++;
|
||||
}
|
||||
|
||||
value get_return_value(int offset)
|
||||
{
|
||||
if (!valid_state())
|
||||
{
|
||||
throw std::runtime_error("Invalid lua state");
|
||||
}
|
||||
|
||||
const auto state = *game::hks::lua_state;
|
||||
const auto top = &state->top[-1 - offset];
|
||||
const auto top = &state->m_apistack.top[-1 - offset];
|
||||
return *top;
|
||||
}
|
||||
|
||||
switch (top->t)
|
||||
void call_script_function(const function& function, const arguments& arguments)
|
||||
{
|
||||
case game::hks::HksObjectType::TBOOLEAN:
|
||||
const auto state = *game::hks::lua_state;
|
||||
|
||||
stack_isolation _;
|
||||
for (auto i = arguments.rbegin(); i != arguments.rend(); ++i)
|
||||
{
|
||||
return {top->v.boolean};
|
||||
push_value(*i);
|
||||
}
|
||||
case game::hks::HksObjectType::TNUMBER:
|
||||
push_value(function);
|
||||
|
||||
enable_error_hook();
|
||||
const auto __ = gsl::finally([]()
|
||||
{
|
||||
const auto number = top->v.number;
|
||||
if (is_integer(number))
|
||||
disable_error_hook();
|
||||
});
|
||||
|
||||
// Not sure about this
|
||||
|
||||
try
|
||||
{
|
||||
return {static_cast<int>(top->v.number)};
|
||||
game::hks::vm_call_internal(state, static_cast<int>(arguments.size()), 0, 0);
|
||||
}
|
||||
else
|
||||
catch (const std::exception& e)
|
||||
{
|
||||
return {top->v.number};
|
||||
}
|
||||
}
|
||||
case game::hks::HksObjectType::TSTRING:
|
||||
{
|
||||
const auto value = top->v.str->m_data;
|
||||
return {std::string(value)};
|
||||
}
|
||||
case game::hks::HksObjectType::TLIGHTUSERDATA:
|
||||
{
|
||||
return lightuserdata{top->v.ptr};
|
||||
}
|
||||
default:
|
||||
{
|
||||
return {};
|
||||
}
|
||||
throw std::runtime_error(std::string("Error executing script function: ") + e.what());
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<value> call(const std::string& name, const arguments& arguments)
|
||||
value get_field(const userdata& self, const value& key)
|
||||
{
|
||||
if (!valid_state())
|
||||
const auto state = *game::hks::lua_state;
|
||||
|
||||
stack_isolation _;
|
||||
push_value(key);
|
||||
|
||||
enable_error_hook();
|
||||
const auto __ = gsl::finally([]()
|
||||
{
|
||||
throw std::runtime_error("Invalid lua state");
|
||||
disable_error_hook();
|
||||
});
|
||||
|
||||
game::hks::HksObject value{};
|
||||
game::hks::HksObject userdata{};
|
||||
userdata.t = game::hks::TUSERDATA;
|
||||
userdata.v.ptr = self.ptr;
|
||||
|
||||
try
|
||||
{
|
||||
game::hks::hks_obj_getfield(&value, state, &userdata, &state->m_apistack.top[-1]);
|
||||
return value;
|
||||
}
|
||||
catch (const std::exception& e)
|
||||
{
|
||||
throw std::runtime_error(std::string("Error getting userdata field: ") + e.what());
|
||||
}
|
||||
}
|
||||
|
||||
value get_field(const table& self, const value& key)
|
||||
{
|
||||
const auto state = *game::hks::lua_state;
|
||||
|
||||
stack_isolation _;
|
||||
push_value(key);
|
||||
|
||||
enable_error_hook();
|
||||
const auto __ = gsl::finally([]()
|
||||
{
|
||||
disable_error_hook();
|
||||
});
|
||||
|
||||
game::hks::HksObject value{};
|
||||
game::hks::HksObject userdata{};
|
||||
userdata.t = game::hks::TTABLE;
|
||||
userdata.v.ptr = self.ptr;
|
||||
|
||||
try
|
||||
{
|
||||
game::hks::hks_obj_getfield(&value, state, &userdata, &state->m_apistack.top[-1]);
|
||||
return value;
|
||||
}
|
||||
catch (const std::exception& e)
|
||||
{
|
||||
throw std::runtime_error(std::string("Error getting table field: ") + e.what());
|
||||
}
|
||||
}
|
||||
|
||||
void set_field(const userdata& self, const value& key, const value& value)
|
||||
{
|
||||
const auto state = *game::hks::lua_state;
|
||||
|
||||
stack_isolation _;
|
||||
|
||||
enable_error_hook();
|
||||
const auto __ = gsl::finally([]()
|
||||
{
|
||||
disable_error_hook();
|
||||
});
|
||||
|
||||
game::hks::HksObject userdata{};
|
||||
userdata.t = game::hks::TUSERDATA;
|
||||
userdata.v.ptr = self.ptr;
|
||||
|
||||
try
|
||||
{
|
||||
game::hks::hks_obj_settable(state, &userdata, &key.get_raw(), &value.get_raw());
|
||||
}
|
||||
catch (const std::exception& e)
|
||||
{
|
||||
throw std::runtime_error(std::string("Error setting userdata field: ") + e.what());
|
||||
}
|
||||
}
|
||||
|
||||
void set_field(const table& self, const value& key, const value& value)
|
||||
{
|
||||
const auto state = *game::hks::lua_state;
|
||||
|
||||
stack_isolation _;
|
||||
|
||||
enable_error_hook();
|
||||
const auto __ = gsl::finally([]()
|
||||
{
|
||||
disable_error_hook();
|
||||
});
|
||||
|
||||
game::hks::HksObject userdata{};
|
||||
userdata.t = game::hks::TTABLE;
|
||||
userdata.v.ptr = self.ptr;
|
||||
|
||||
try
|
||||
{
|
||||
game::hks::hks_obj_settable(state, &userdata, &key.get_raw(), &value.get_raw());
|
||||
}
|
||||
catch (const std::exception& e)
|
||||
{
|
||||
throw std::runtime_error(std::string("Error setting table field: ") + e.what());
|
||||
}
|
||||
}
|
||||
|
||||
arguments call_method(const userdata& self, const std::string& name, const arguments& arguments)
|
||||
{
|
||||
const auto function = ui_scripting::find_method(name);
|
||||
if (!function)
|
||||
{
|
||||
throw std::runtime_error("Function " + name + " not found");
|
||||
}
|
||||
|
||||
stack_isolation _;
|
||||
push_value(self);
|
||||
for (auto i = arguments.rbegin(); i != arguments.rend(); ++i)
|
||||
{
|
||||
push_value(*i);
|
||||
}
|
||||
|
||||
enable_error_hook();
|
||||
const auto __ = gsl::finally([]()
|
||||
{
|
||||
disable_error_hook();
|
||||
});
|
||||
|
||||
try
|
||||
{
|
||||
const auto count = function(*game::hks::lua_state);
|
||||
std::vector<value> values;
|
||||
|
||||
for (auto i = 0; i < count; i++)
|
||||
{
|
||||
values.push_back(get_return_value(i));
|
||||
}
|
||||
|
||||
if (values.size() == 0)
|
||||
{
|
||||
values.push_back({});
|
||||
}
|
||||
|
||||
return values;
|
||||
}
|
||||
catch (const std::exception& e)
|
||||
{
|
||||
throw std::runtime_error("Error executing method " + name + ": " + e.what());
|
||||
}
|
||||
}
|
||||
|
||||
arguments call(const std::string& name, const arguments& arguments)
|
||||
{
|
||||
const auto function = ui_scripting::find_function(name);
|
||||
if (!function)
|
||||
{
|
||||
@ -131,19 +214,17 @@ namespace ui_scripting
|
||||
}
|
||||
|
||||
stack_isolation _;
|
||||
for (auto i = arguments.rbegin(); i != arguments.rend(); ++i)
|
||||
{
|
||||
push_value(*i);
|
||||
}
|
||||
|
||||
enable_error_hook();
|
||||
const auto __ = gsl::finally([]()
|
||||
{
|
||||
disable_error_hook();
|
||||
});
|
||||
|
||||
enable_error_hook();
|
||||
|
||||
for (const auto& value_ : arguments)
|
||||
{
|
||||
push_value(value_);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
const auto count = function(*game::hks::lua_state);
|
||||
|
@ -1,7 +1,20 @@
|
||||
#pragma once
|
||||
#include "game/game.hpp"
|
||||
#include "types.hpp"
|
||||
#include "value.hpp"
|
||||
|
||||
namespace ui_scripting
|
||||
{
|
||||
std::vector<value> call(const std::string& name, const arguments& arguments);
|
||||
void push_value(const value& value);
|
||||
value get_return_value(int offset);
|
||||
|
||||
void call_script_function(const function& function, const arguments& arguments);
|
||||
|
||||
value get_field(const userdata& self, const value& key);
|
||||
value get_field(const table& self, const value& key);
|
||||
void set_field(const userdata& self, const value& key, const value& value);
|
||||
void set_field(const table& self, const value& key, const value& value);
|
||||
|
||||
arguments call_method(const userdata& self, const std::string& name, const arguments& arguments);
|
||||
arguments call(const std::string& name, const arguments& arguments);
|
||||
}
|
||||
|
@ -1,6 +1,7 @@
|
||||
#include <std_include.hpp>
|
||||
#include "context.hpp"
|
||||
#include "error.hpp"
|
||||
#include "value_conversion.hpp"
|
||||
#include "../../scripting/execution.hpp"
|
||||
#include "../value.hpp"
|
||||
#include "../execution.hpp"
|
||||
@ -26,7 +27,7 @@ namespace ui_scripting::lua
|
||||
{
|
||||
const auto animation_script = utils::nt::load_resource(LUA_ANIMATION_SCRIPT);
|
||||
|
||||
scripting::script_value convert(const sol::lua_value& value)
|
||||
scripting::script_value script_convert(const sol::lua_value& value)
|
||||
{
|
||||
if (value.is<int>())
|
||||
{
|
||||
@ -505,14 +506,7 @@ namespace ui_scripting::lua
|
||||
|
||||
for (auto arg : va)
|
||||
{
|
||||
if (arg.is<value>())
|
||||
{
|
||||
event.arguments.push_back(arg.as<value>());
|
||||
}
|
||||
else
|
||||
{
|
||||
event.arguments.push_back({});
|
||||
}
|
||||
event.arguments.push_back(convert({s, arg}));
|
||||
}
|
||||
|
||||
notify(event);
|
||||
@ -529,9 +523,9 @@ namespace ui_scripting::lua
|
||||
}
|
||||
);
|
||||
|
||||
element_type[sol::meta_function::new_index] = [](element& element, const std::string& attribute, const value& value)
|
||||
element_type[sol::meta_function::new_index] = [](element& element, const sol::this_state s, const std::string& attribute, const sol::lua_value& value)
|
||||
{
|
||||
element.attributes[attribute] = value;
|
||||
element.attributes[attribute] = convert({s, value});
|
||||
};
|
||||
|
||||
element_type[sol::meta_function::index] = [](element& element, const sol::this_state s, const std::string& attribute)
|
||||
@ -541,7 +535,7 @@ namespace ui_scripting::lua
|
||||
return sol::lua_value{s, sol::lua_nil};
|
||||
}
|
||||
|
||||
return sol::lua_value{s, element.attributes[attribute]};
|
||||
return sol::lua_value{s, convert(s, element.attributes[attribute])};
|
||||
};
|
||||
|
||||
auto menu_type = state.new_usertype<menu>("menu");
|
||||
@ -579,14 +573,7 @@ namespace ui_scripting::lua
|
||||
|
||||
for (auto arg : va)
|
||||
{
|
||||
if (arg.is<value>())
|
||||
{
|
||||
event.arguments.push_back(arg.as<value>());
|
||||
}
|
||||
else
|
||||
{
|
||||
event.arguments.push_back({});
|
||||
}
|
||||
event.arguments.push_back(convert({s, arg}));
|
||||
}
|
||||
|
||||
notify(event);
|
||||
@ -655,11 +642,13 @@ namespace ui_scripting::lua
|
||||
menu.close();
|
||||
};
|
||||
|
||||
menu_type["getelement"] = [](menu& menu, const sol::this_state s, const value& value, const std::string& attribute)
|
||||
menu_type["getelement"] = [](menu& menu, const sol::this_state s, const sol::lua_value& value, const std::string& attribute)
|
||||
{
|
||||
const auto value_ = convert({s, value});
|
||||
|
||||
for (const auto& element : menu.children)
|
||||
{
|
||||
if (element->attributes.find(attribute) != element->attributes.end() && element->attributes[attribute] == value)
|
||||
if (element->attributes.find(attribute) != element->attributes.end() && element->attributes[attribute] == value_)
|
||||
{
|
||||
return sol::lua_value{s, element};
|
||||
}
|
||||
@ -670,13 +659,14 @@ namespace ui_scripting::lua
|
||||
|
||||
menu_type["getelements"] = sol::overload
|
||||
(
|
||||
[](menu& menu, const sol::this_state s, const value& value, const std::string& attribute)
|
||||
[](menu& menu, const sol::this_state s, const sol::lua_value& value, const std::string& attribute)
|
||||
{
|
||||
const auto value_ = convert({s, value});
|
||||
auto result = sol::table::create(s.lua_state());
|
||||
|
||||
for (const auto& element : menu.children)
|
||||
{
|
||||
if (element->attributes.find(attribute) != element->attributes.end() && element->attributes[attribute] == value)
|
||||
if (element->attributes.find(attribute) != element->attributes.end() && element->attributes[attribute] == value_)
|
||||
{
|
||||
result.add(element);
|
||||
}
|
||||
@ -713,11 +703,13 @@ namespace ui_scripting::lua
|
||||
return sol::lua_value{s, &menus[name]};
|
||||
};
|
||||
|
||||
game_type["getelement"] = [](const game&, const sol::this_state s, const value& value, const std::string& attribute)
|
||||
game_type["getelement"] = [](const game&, const sol::this_state s, const sol::lua_value& value, const std::string& attribute)
|
||||
{
|
||||
const auto value_ = convert({s, value});
|
||||
|
||||
for (const auto& element : elements)
|
||||
{
|
||||
if (element->attributes.find(attribute) != element->attributes.end() && element->attributes[attribute] == value)
|
||||
if (element->attributes.find(attribute) != element->attributes.end() && element->attributes[attribute] == value_)
|
||||
{
|
||||
return sol::lua_value{s, element};
|
||||
}
|
||||
@ -728,13 +720,14 @@ namespace ui_scripting::lua
|
||||
|
||||
game_type["getelements"] = sol::overload
|
||||
(
|
||||
[](const game&, const sol::this_state s, const value& value, const std::string& attribute)
|
||||
[](const game&, const sol::this_state s, const sol::lua_value& value, const std::string& attribute)
|
||||
{
|
||||
const auto value_ = convert({s, value});
|
||||
auto result = sol::table::create(s.lua_state());
|
||||
|
||||
for (const auto& element : elements)
|
||||
{
|
||||
if (element->attributes.find(attribute) != element->attributes.end() && element->attributes[attribute] == value)
|
||||
if (element->attributes.find(attribute) != element->attributes.end() && element->attributes[attribute] == value_)
|
||||
{
|
||||
result.add(element);
|
||||
}
|
||||
@ -974,29 +967,128 @@ namespace ui_scripting::lua
|
||||
{
|
||||
return [name](const game&, const sol::this_state s, sol::variadic_args va)
|
||||
{
|
||||
arguments arguments;
|
||||
arguments arguments{};
|
||||
|
||||
for (auto arg : va)
|
||||
{
|
||||
arguments.push_back(arg);
|
||||
arguments.push_back(convert({s, arg}));
|
||||
}
|
||||
|
||||
const auto values = call(name, arguments);
|
||||
return sol::as_returns(values);
|
||||
std::vector<sol::lua_value> returns;
|
||||
|
||||
for (const auto& value : values)
|
||||
{
|
||||
returns.push_back(convert(s, value));
|
||||
}
|
||||
|
||||
return sol::as_returns(returns);
|
||||
};
|
||||
};
|
||||
|
||||
game_type["call"] = [](const game&, const sol::this_state s, const std::string& name, sol::variadic_args va)
|
||||
{
|
||||
arguments arguments;
|
||||
arguments arguments{};
|
||||
|
||||
for (auto arg : va)
|
||||
{
|
||||
arguments.push_back(arg);
|
||||
arguments.push_back(convert({s, arg}));
|
||||
}
|
||||
|
||||
const auto values = call(name, arguments);
|
||||
return sol::as_returns(values);
|
||||
std::vector<sol::lua_value> returns;
|
||||
|
||||
for (const auto& value : values)
|
||||
{
|
||||
returns.push_back(convert(s, value));
|
||||
}
|
||||
|
||||
return sol::as_returns(returns);
|
||||
};
|
||||
|
||||
auto userdata_type = state.new_usertype<userdata>("userdata_");
|
||||
|
||||
for (const auto method : methods)
|
||||
{
|
||||
const auto name = method.first;
|
||||
|
||||
userdata_type[name] = [name](const userdata& userdata, const sol::this_state s, sol::variadic_args va)
|
||||
{
|
||||
arguments arguments{};
|
||||
|
||||
for (auto arg : va)
|
||||
{
|
||||
arguments.push_back(convert({s, arg}));
|
||||
}
|
||||
|
||||
const auto values = call_method(userdata, name, arguments);
|
||||
std::vector<sol::lua_value> returns;
|
||||
|
||||
for (const auto& value : values)
|
||||
{
|
||||
returns.push_back(convert(s, value));
|
||||
}
|
||||
|
||||
return sol::as_returns(returns);
|
||||
};
|
||||
}
|
||||
|
||||
userdata_type[sol::meta_function::index] = [](const userdata& userdata, const sol::this_state s,
|
||||
const std::string& name)
|
||||
{
|
||||
return convert(s, userdata.get(name));
|
||||
};
|
||||
|
||||
userdata_type[sol::meta_function::new_index] = [](const userdata& userdata, const sol::this_state s,
|
||||
const std::string& name, const sol::lua_value& value)
|
||||
{
|
||||
userdata.set(name, convert({s, value}));
|
||||
};
|
||||
|
||||
auto table_type = state.new_usertype<table>("table_");
|
||||
|
||||
table_type[sol::meta_function::index] = [](const table& table, const sol::this_state s,
|
||||
const std::string& name)
|
||||
{
|
||||
return convert(s, table.get(name));
|
||||
};
|
||||
|
||||
table_type[sol::meta_function::new_index] = [](const table& table, const sol::this_state s,
|
||||
const std::string& name, const sol::lua_value& value)
|
||||
{
|
||||
table.set(name, convert({s, value}));
|
||||
};
|
||||
|
||||
table_type[sol::meta_function::length] = [](const table& table)
|
||||
{
|
||||
return table.size();
|
||||
};
|
||||
|
||||
table_type["getkeys"] = [](const table& table, const sol::this_state s)
|
||||
{
|
||||
std::vector<sol::lua_value> result;
|
||||
const auto keys = table.get_keys();
|
||||
|
||||
for (const auto& key : keys)
|
||||
{
|
||||
result.push_back(convert(s, key));
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
auto function_type = state.new_usertype<function>("function");
|
||||
|
||||
function_type[sol::meta_function::call] = [](const function& function, const sol::this_state s, sol::variadic_args va)
|
||||
{
|
||||
arguments arguments{};
|
||||
|
||||
for (auto arg : va)
|
||||
{
|
||||
arguments.push_back(convert({s, arg}));
|
||||
}
|
||||
|
||||
function.call(arguments);
|
||||
};
|
||||
|
||||
struct player
|
||||
@ -1018,7 +1110,7 @@ namespace ui_scripting::lua
|
||||
|
||||
for (auto arg : args)
|
||||
{
|
||||
arguments.push_back(convert({s, arg}));
|
||||
arguments.push_back(script_convert({s, arg}));
|
||||
}
|
||||
|
||||
const auto player_value = scripting::call("getentbynum", {0});
|
||||
|
@ -3,6 +3,7 @@
|
||||
#include "error.hpp"
|
||||
|
||||
#include "event_handler.hpp"
|
||||
#include "value_conversion.hpp"
|
||||
|
||||
namespace ui_scripting::lua
|
||||
{
|
||||
@ -167,14 +168,7 @@ namespace ui_scripting::lua
|
||||
|
||||
for (const auto& argument : event.arguments)
|
||||
{
|
||||
if (argument.index() > 0)
|
||||
{
|
||||
arguments.emplace_back(argument);
|
||||
}
|
||||
else
|
||||
{
|
||||
arguments.emplace_back(sol::lua_value{this->state_, sol::lua_nil});
|
||||
}
|
||||
arguments.emplace_back(convert(this->state_, argument));
|
||||
}
|
||||
|
||||
return arguments;
|
||||
|
121
src/client/game/ui_scripting/lua/value_conversion.cpp
Normal file
121
src/client/game/ui_scripting/lua/value_conversion.cpp
Normal file
@ -0,0 +1,121 @@
|
||||
#include <std_include.hpp>
|
||||
#include "value_conversion.hpp"
|
||||
#include "../execution.hpp"
|
||||
|
||||
namespace ui_scripting::lua
|
||||
{
|
||||
namespace
|
||||
{
|
||||
table convert_table(const sol::table& t)
|
||||
{
|
||||
table res{};
|
||||
|
||||
t.for_each([res](const sol::object& key, const sol::object& value)
|
||||
{
|
||||
res.set(convert(key), convert(value));
|
||||
});
|
||||
|
||||
return res;
|
||||
}
|
||||
}
|
||||
|
||||
value convert(const sol::lua_value& value)
|
||||
{
|
||||
if (value.is<int>())
|
||||
{
|
||||
return {value.as<int>()};
|
||||
}
|
||||
|
||||
if (value.is<unsigned int>())
|
||||
{
|
||||
return {value.as<unsigned int>()};
|
||||
}
|
||||
|
||||
if (value.is<bool>())
|
||||
{
|
||||
return {value.as<bool>()};
|
||||
}
|
||||
|
||||
if (value.is<double>())
|
||||
{
|
||||
return {value.as<double>()};
|
||||
}
|
||||
|
||||
if (value.is<float>())
|
||||
{
|
||||
return {value.as<float>()};
|
||||
}
|
||||
|
||||
if (value.is<std::string>())
|
||||
{
|
||||
return {value.as<std::string>()};
|
||||
}
|
||||
|
||||
if (value.is<lightuserdata>())
|
||||
{
|
||||
return {value.as<lightuserdata>()};
|
||||
}
|
||||
|
||||
if (value.is<userdata>())
|
||||
{
|
||||
return {value.as<userdata>()};
|
||||
}
|
||||
|
||||
if (value.is<table>())
|
||||
{
|
||||
return {value.as<table>()};
|
||||
}
|
||||
|
||||
if (value.is<function>())
|
||||
{
|
||||
return {value.as<function>()};
|
||||
}
|
||||
|
||||
if (value.is<sol::table>())
|
||||
{
|
||||
return {convert_table(value.as<sol::table>())};
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
sol::lua_value convert(lua_State* state, const value& value)
|
||||
{
|
||||
if (value.is<int>())
|
||||
{
|
||||
return {state, value.as<int>()};
|
||||
}
|
||||
|
||||
if (value.is<float>())
|
||||
{
|
||||
return {state, value.as<float>()};
|
||||
}
|
||||
|
||||
if (value.is<std::string>())
|
||||
{
|
||||
return {state, value.as<std::string>()};
|
||||
}
|
||||
|
||||
if (value.is<lightuserdata>())
|
||||
{
|
||||
return {state, value.as<lightuserdata>()};
|
||||
}
|
||||
|
||||
if (value.is<userdata>())
|
||||
{
|
||||
return {state, value.as<userdata>()};
|
||||
}
|
||||
|
||||
if (value.is<table>())
|
||||
{
|
||||
return {state, value.as<table>()};
|
||||
}
|
||||
|
||||
if (value.is<function>())
|
||||
{
|
||||
return {state, value.as<function>()};
|
||||
}
|
||||
|
||||
return {state, sol::lua_nil};
|
||||
}
|
||||
}
|
9
src/client/game/ui_scripting/lua/value_conversion.hpp
Normal file
9
src/client/game/ui_scripting/lua/value_conversion.hpp
Normal file
@ -0,0 +1,9 @@
|
||||
#pragma once
|
||||
|
||||
#include "context.hpp"
|
||||
|
||||
namespace ui_scripting::lua
|
||||
{
|
||||
value convert(const sol::lua_value& value);
|
||||
sol::lua_value convert(lua_State* state, const value& value);
|
||||
}
|
@ -7,24 +7,24 @@ namespace ui_scripting
|
||||
{
|
||||
const auto state = *game::hks::lua_state;
|
||||
|
||||
this->top_ = state->top;
|
||||
this->base_ = state->base;
|
||||
this->alloc_top_ = state->alloc_top;
|
||||
this->bottom_ = state->bottom;
|
||||
this->top_ = state->m_apistack.top;
|
||||
this->base_ = state->m_apistack.base;
|
||||
this->alloc_top_ = state->m_apistack.alloc_top;
|
||||
this->bottom_ = state->m_apistack.bottom;
|
||||
|
||||
state->top = this->stack_;
|
||||
state->base = state->top;
|
||||
state->alloc_top = &this->stack_[ARRAYSIZE(this->stack_) - 1];
|
||||
state->bottom = state->top;
|
||||
state->m_apistack.top = this->stack_;
|
||||
state->m_apistack.base = state->m_apistack.top;
|
||||
state->m_apistack.alloc_top = &this->stack_[ARRAYSIZE(this->stack_) - 1];
|
||||
state->m_apistack.bottom = state->m_apistack.top;
|
||||
}
|
||||
|
||||
stack_isolation::~stack_isolation()
|
||||
{
|
||||
const auto state = *game::hks::lua_state;
|
||||
|
||||
state->top = this->top_;
|
||||
state->base = this->base_;
|
||||
state->alloc_top = this->alloc_top_;
|
||||
state->bottom = this->bottom_;
|
||||
state->m_apistack.top = this->top_;
|
||||
state->m_apistack.base = this->base_;
|
||||
state->m_apistack.alloc_top = this->alloc_top_;
|
||||
state->m_apistack.bottom = this->bottom_;
|
||||
}
|
||||
}
|
||||
|
98
src/client/game/ui_scripting/types.cpp
Normal file
98
src/client/game/ui_scripting/types.cpp
Normal file
@ -0,0 +1,98 @@
|
||||
#include <std_include.hpp>
|
||||
#include "types.hpp"
|
||||
#include "execution.hpp"
|
||||
|
||||
namespace ui_scripting
|
||||
{
|
||||
/***************************************************************
|
||||
* Lightuserdata
|
||||
**************************************************************/
|
||||
|
||||
lightuserdata::lightuserdata(void* ptr_)
|
||||
: ptr(ptr_)
|
||||
{
|
||||
}
|
||||
|
||||
/***************************************************************
|
||||
* Userdata
|
||||
**************************************************************/
|
||||
|
||||
userdata::userdata(void* ptr_)
|
||||
: ptr(ptr_)
|
||||
{
|
||||
}
|
||||
|
||||
void userdata::set(const value& key, const value& value) const
|
||||
{
|
||||
set_field(*this, key, value);
|
||||
}
|
||||
|
||||
value userdata::get(const value& key) const
|
||||
{
|
||||
return get_field(*this, key);
|
||||
}
|
||||
|
||||
arguments userdata::call(const std::string& name, const arguments& arguments) const
|
||||
{
|
||||
return call_method(this->ptr, name, arguments);
|
||||
}
|
||||
|
||||
/***************************************************************
|
||||
* Table
|
||||
**************************************************************/
|
||||
|
||||
table::table()
|
||||
{
|
||||
const auto state = *game::hks::lua_state;
|
||||
this->ptr = game::hks::Hashtable_Create(state, 0, 0);
|
||||
}
|
||||
|
||||
table::table(game::hks::HashTable* ptr_)
|
||||
: ptr(ptr_)
|
||||
{
|
||||
}
|
||||
|
||||
arguments table::get_keys() const
|
||||
{
|
||||
arguments keys{};
|
||||
auto current = this->ptr->m_hashPart;
|
||||
|
||||
while (current->m_key.t)
|
||||
{
|
||||
keys.push_back(current->m_key);
|
||||
current++;
|
||||
}
|
||||
|
||||
return keys;
|
||||
}
|
||||
|
||||
unsigned int table::size() const
|
||||
{
|
||||
return static_cast<unsigned int>(this->get_keys().size());
|
||||
}
|
||||
|
||||
void table::set(const value& key, const value& value) const
|
||||
{
|
||||
set_field(*this, key, value);
|
||||
}
|
||||
|
||||
value table::get(const value& key) const
|
||||
{
|
||||
return get_field(*this, key);
|
||||
}
|
||||
|
||||
/***************************************************************
|
||||
* Function
|
||||
**************************************************************/
|
||||
|
||||
function::function(void* ptr_)
|
||||
: ptr(ptr_)
|
||||
{
|
||||
}
|
||||
|
||||
arguments function::call(const arguments& arguments) const
|
||||
{
|
||||
call_script_function(*this, arguments);
|
||||
return {};
|
||||
}
|
||||
}
|
50
src/client/game/ui_scripting/types.hpp
Normal file
50
src/client/game/ui_scripting/types.hpp
Normal file
@ -0,0 +1,50 @@
|
||||
#pragma once
|
||||
#include "game/game.hpp"
|
||||
#include "value.hpp"
|
||||
|
||||
namespace ui_scripting
|
||||
{
|
||||
class lightuserdata
|
||||
{
|
||||
public:
|
||||
lightuserdata(void*);
|
||||
void* ptr;
|
||||
};
|
||||
|
||||
class userdata
|
||||
{
|
||||
public:
|
||||
userdata(void*);
|
||||
|
||||
value get(const value& key) const;
|
||||
void set(const value& key, const value& value) const;
|
||||
arguments call(const std::string& name, const arguments& arguments) const;
|
||||
|
||||
void* ptr;
|
||||
};
|
||||
|
||||
class table
|
||||
{
|
||||
public:
|
||||
table();
|
||||
table(game::hks::HashTable* ptr_);
|
||||
|
||||
arguments get_keys() const;
|
||||
unsigned int size() const;
|
||||
|
||||
value get(const value& key) const;
|
||||
void set(const value& key, const value& value) const;
|
||||
|
||||
game::hks::HashTable* ptr;
|
||||
};
|
||||
|
||||
class function
|
||||
{
|
||||
public:
|
||||
function(void*);
|
||||
|
||||
arguments call(const arguments& arguments) const;
|
||||
|
||||
void* ptr;
|
||||
};
|
||||
}
|
280
src/client/game/ui_scripting/value.cpp
Normal file
280
src/client/game/ui_scripting/value.cpp
Normal file
@ -0,0 +1,280 @@
|
||||
#include <std_include.hpp>
|
||||
#include "execution.hpp"
|
||||
#include "types.hpp"
|
||||
#include "stack_isolation.hpp"
|
||||
#include "value.hpp"
|
||||
|
||||
namespace ui_scripting
|
||||
{
|
||||
/***************************************************************
|
||||
* Constructors
|
||||
**************************************************************/
|
||||
|
||||
value::value(const game::hks::HksObject& value)
|
||||
: value_(value)
|
||||
{
|
||||
}
|
||||
|
||||
value::value(const int value)
|
||||
{
|
||||
game::hks::HksObject obj{};
|
||||
obj.t = game::hks::TNUMBER;
|
||||
obj.v.number = static_cast<float>(value);
|
||||
|
||||
this->value_ = obj;
|
||||
}
|
||||
|
||||
value::value(const unsigned int value)
|
||||
{
|
||||
game::hks::HksObject obj{};
|
||||
obj.t = game::hks::TNUMBER;
|
||||
obj.v.number = static_cast<float>(value);
|
||||
|
||||
this->value_ = obj;
|
||||
}
|
||||
|
||||
value::value(const bool value)
|
||||
: value(static_cast<unsigned>(value))
|
||||
{
|
||||
}
|
||||
|
||||
value::value(const float value)
|
||||
{
|
||||
game::hks::HksObject obj{};
|
||||
obj.t = game::hks::TNUMBER;
|
||||
obj.v.number = static_cast<float>(value);
|
||||
|
||||
this->value_ = obj;
|
||||
}
|
||||
|
||||
value::value(const double value)
|
||||
: value(static_cast<float>(value))
|
||||
{
|
||||
}
|
||||
|
||||
value::value(const char* value)
|
||||
{
|
||||
game::hks::HksObject obj{};
|
||||
stack_isolation _;
|
||||
|
||||
const auto state = *game::hks::lua_state;
|
||||
game::hks::hksi_lua_pushlstring(state, value, (unsigned int)strlen(value));
|
||||
obj = state->m_apistack.top[-1];
|
||||
|
||||
this->value_ = obj;
|
||||
}
|
||||
|
||||
value::value(const std::string& value)
|
||||
: value(value.data())
|
||||
{
|
||||
}
|
||||
|
||||
value::value(const lightuserdata& value)
|
||||
{
|
||||
this->value_.t = game::hks::TLIGHTUSERDATA;
|
||||
this->value_.v.ptr = value.ptr;
|
||||
}
|
||||
|
||||
value::value(const userdata& value)
|
||||
{
|
||||
this->value_.t = game::hks::TUSERDATA;
|
||||
this->value_.v.ptr = value.ptr;
|
||||
}
|
||||
|
||||
value::value(const table& value)
|
||||
{
|
||||
this->value_.t = game::hks::TTABLE;
|
||||
this->value_.v.ptr = value.ptr;
|
||||
}
|
||||
|
||||
value::value(const function& value)
|
||||
{
|
||||
this->value_.t = game::hks::TIFUNCTION;
|
||||
this->value_.v.ptr = value.ptr;
|
||||
}
|
||||
|
||||
/***************************************************************
|
||||
* Integer
|
||||
**************************************************************/
|
||||
|
||||
template <>
|
||||
bool value::is<int>() const
|
||||
{
|
||||
return this->get_raw().t == game::hks::TNUMBER;
|
||||
}
|
||||
|
||||
template <>
|
||||
bool value::is<unsigned int>() const
|
||||
{
|
||||
return this->is<int>();
|
||||
}
|
||||
|
||||
template <>
|
||||
bool value::is<bool>() const
|
||||
{
|
||||
return this->is<int>();
|
||||
}
|
||||
|
||||
template <>
|
||||
int value::get() const
|
||||
{
|
||||
return static_cast<int>(this->get_raw().v.number);
|
||||
}
|
||||
|
||||
template <>
|
||||
unsigned int value::get() const
|
||||
{
|
||||
return static_cast<unsigned int>(this->get_raw().v.number);
|
||||
}
|
||||
|
||||
template <>
|
||||
bool value::get() const
|
||||
{
|
||||
return this->get_raw().v.native != 0;
|
||||
}
|
||||
|
||||
/***************************************************************
|
||||
* Float
|
||||
**************************************************************/
|
||||
|
||||
template <>
|
||||
bool value::is<float>() const
|
||||
{
|
||||
return this->get_raw().t == game::hks::TNUMBER;
|
||||
}
|
||||
|
||||
template <>
|
||||
bool value::is<double>() const
|
||||
{
|
||||
return this->is<float>();
|
||||
}
|
||||
|
||||
template <>
|
||||
float value::get() const
|
||||
{
|
||||
return this->get_raw().v.number;
|
||||
}
|
||||
|
||||
template <>
|
||||
double value::get() const
|
||||
{
|
||||
return static_cast<double>(this->get_raw().v.number);
|
||||
}
|
||||
|
||||
/***************************************************************
|
||||
* String
|
||||
**************************************************************/
|
||||
|
||||
template <>
|
||||
bool value::is<const char*>() const
|
||||
{
|
||||
return this->get_raw().t == game::hks::TSTRING;
|
||||
}
|
||||
|
||||
template <>
|
||||
bool value::is<std::string>() const
|
||||
{
|
||||
return this->is<const char*>();
|
||||
}
|
||||
|
||||
template <>
|
||||
const char* value::get() const
|
||||
{
|
||||
return this->get_raw().v.str->m_data;
|
||||
}
|
||||
|
||||
template <>
|
||||
std::string value::get() const
|
||||
{
|
||||
return this->get<const char*>();
|
||||
}
|
||||
|
||||
bool value::operator==(const value& other)
|
||||
{
|
||||
if (this->get_raw().t != other.get_raw().t)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (this->get_raw().t == game::hks::TSTRING)
|
||||
{
|
||||
return this->get<std::string>() == other.get<std::string>();
|
||||
}
|
||||
|
||||
return this->get_raw().v.native == other.get_raw().v.native;
|
||||
}
|
||||
|
||||
/***************************************************************
|
||||
* Lightuserdata
|
||||
**************************************************************/
|
||||
|
||||
template <>
|
||||
bool value::is<lightuserdata>() const
|
||||
{
|
||||
return this->get_raw().t == game::hks::TLIGHTUSERDATA;
|
||||
}
|
||||
|
||||
template <>
|
||||
lightuserdata value::get() const
|
||||
{
|
||||
return this->get_raw().v.ptr;
|
||||
}
|
||||
|
||||
/***************************************************************
|
||||
* Userdata
|
||||
**************************************************************/
|
||||
|
||||
template <>
|
||||
bool value::is<userdata>() const
|
||||
{
|
||||
return this->get_raw().t == game::hks::TUSERDATA;
|
||||
}
|
||||
|
||||
template <>
|
||||
userdata value::get() const
|
||||
{
|
||||
return this->get_raw().v.ptr;
|
||||
}
|
||||
|
||||
/***************************************************************
|
||||
* Table
|
||||
**************************************************************/
|
||||
|
||||
template <>
|
||||
bool value::is<table>() const
|
||||
{
|
||||
return this->get_raw().t == game::hks::TTABLE;
|
||||
}
|
||||
|
||||
template <>
|
||||
table value::get() const
|
||||
{
|
||||
return this->get_raw().v.table;
|
||||
}
|
||||
|
||||
/***************************************************************
|
||||
* Function
|
||||
**************************************************************/
|
||||
|
||||
template <>
|
||||
bool value::is<function>() const
|
||||
{
|
||||
return this->get_raw().t == game::hks::TIFUNCTION
|
||||
|| this->get_raw().t == game::hks::TCFUNCTION;
|
||||
}
|
||||
|
||||
template <>
|
||||
function value::get() const
|
||||
{
|
||||
return this->get_raw().v.ptr;
|
||||
}
|
||||
|
||||
/***************************************************************
|
||||
*
|
||||
**************************************************************/
|
||||
|
||||
const game::hks::HksObject& value::get_raw() const
|
||||
{
|
||||
return this->value_;
|
||||
}
|
||||
}
|
@ -3,15 +3,56 @@
|
||||
|
||||
namespace ui_scripting
|
||||
{
|
||||
struct lightuserdata
|
||||
class lightuserdata;
|
||||
class userdata;
|
||||
class table;
|
||||
class function;
|
||||
|
||||
class value
|
||||
{
|
||||
void* ptr;
|
||||
bool operator==(const lightuserdata other) const noexcept
|
||||
public:
|
||||
value() = default;
|
||||
value(const game::hks::HksObject& value);
|
||||
|
||||
value(int value);
|
||||
value(unsigned int value);
|
||||
value(bool value);
|
||||
|
||||
value(float value);
|
||||
value(double value);
|
||||
|
||||
value(const char* value);
|
||||
value(const std::string& value);
|
||||
|
||||
value(const lightuserdata& value);
|
||||
value(const userdata& value);
|
||||
value(const table& value);
|
||||
value(const function& value);
|
||||
|
||||
bool value::operator==(const value& other);
|
||||
|
||||
template <typename T>
|
||||
bool is() const;
|
||||
|
||||
template <typename T>
|
||||
T as() const
|
||||
{
|
||||
return this->ptr == other.ptr;
|
||||
if (!this->is<T>())
|
||||
{
|
||||
throw std::runtime_error("Invalid type");
|
||||
}
|
||||
|
||||
return get<T>();
|
||||
}
|
||||
|
||||
const game::hks::HksObject& get_raw() const;
|
||||
|
||||
private:
|
||||
template <typename T>
|
||||
T get() const;
|
||||
|
||||
game::hks::HksObject value_{};
|
||||
};
|
||||
|
||||
using value = std::variant<std::monostate, bool, int, float, std::string, lightuserdata>;
|
||||
using arguments = std::vector<value>;
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user