Merge branch 'MihailRis:main' into main
This commit is contained in:
commit
2a9ce15a83
180
CHANGELOG.md
180
CHANGELOG.md
@ -1,6 +1,6 @@
|
||||
# 0.23 - 2024.10.19
|
||||
# 0.24 - 2024.11.07
|
||||
|
||||
[Documentation](https://github.com/MihailRis/VoxelEngine-Cpp/tree/release-0.23/doc/en/main-page.md) for 0.23
|
||||
[Documentation](https://github.com/MihailRis/VoxelEngine-Cpp/tree/release-0.24/doc/en/main-page.md) for 0.24
|
||||
|
||||
Table of contents:
|
||||
|
||||
@ -11,76 +11,128 @@ Table of contents:
|
||||
|
||||
## Added
|
||||
|
||||
- world generation engine instead of hardcoded generator
|
||||
- world generators
|
||||
- core:default
|
||||
- base:demo
|
||||
- block fields (metadata)
|
||||
- resource aliases (resource-aliases.json)
|
||||
- cameras
|
||||
- libraries
|
||||
- generation
|
||||
- bjson
|
||||
- commands:
|
||||
- fragment.save
|
||||
- fragment.crop
|
||||
- blocks:
|
||||
- core:obstacle
|
||||
- core:struct_air
|
||||
- base:coal_ore
|
||||
- settings:
|
||||
- graphics.chunk-max-vertices
|
||||
- graphics.chunk-max-renderers
|
||||
- particles
|
||||
- VEC3 models support
|
||||
- handhold item display
|
||||
- rules
|
||||
- events:
|
||||
- on_block_broken (documented)
|
||||
- on_block_placed (documented)
|
||||
- on_block_interact
|
||||
- libraries:
|
||||
- gfx.particles
|
||||
- utf8
|
||||
- rules
|
||||
- bindings:
|
||||
- player.destroy
|
||||
- player.fast_interaction
|
||||
- water overlay
|
||||
- block models from OBJ or VEC3
|
||||
- bicubic heightmaps interpolation method
|
||||
- unicode escapes support
|
||||
- fragments placements
|
||||
- console commands:
|
||||
- time.daycycle
|
||||
- fragment.place
|
||||
- rule.list
|
||||
- rule.set
|
||||
- text field 'subconsumer'
|
||||
- shader uniforms:
|
||||
- u_lightDir to main shader
|
||||
- u_dayTime to skybox shader
|
||||
- block properties:
|
||||
- surface-replacement
|
||||
- fields
|
||||
- 'parent' property for blocks, items and entities
|
||||
- filesystem entry points:
|
||||
- config
|
||||
- export
|
||||
- lua usertypes:
|
||||
- Heightmap
|
||||
- VoxelFragment
|
||||
- raycast filter
|
||||
- (project) add unit tests framework (gtest)
|
||||
- (project) change project title to VoxelCore
|
||||
- overlay-texture
|
||||
- model-name
|
||||
- item properties:
|
||||
- model-name
|
||||
- 'Open content folder' buttons
|
||||
- 'Background framerate limit' setting
|
||||
|
||||
### Functions
|
||||
|
||||
- debug.print
|
||||
- pack.shared_file
|
||||
- block.get_field
|
||||
- block.set_field
|
||||
- item.caption
|
||||
- core.open_folder
|
||||
- world.get_generator
|
||||
- world.is_open
|
||||
- item.placing_block
|
||||
- item.model_name
|
||||
- item.emission
|
||||
- entities.get_hitbox
|
||||
- utf8.tobytes
|
||||
- utf8.tostring
|
||||
- utf8.length
|
||||
- utf8.codepoint
|
||||
- utf8.encode
|
||||
- utf8.sub
|
||||
- utf8.upper
|
||||
- utf8.lower
|
||||
- file.read_combined_object
|
||||
- fragment:place
|
||||
- rules.create
|
||||
- rules.listen
|
||||
- rules.unlisten
|
||||
- rules.get
|
||||
- rules.set
|
||||
- rules.reset
|
||||
- input.set_enabled
|
||||
- hud._is_content_access
|
||||
- hud._set_content_access
|
||||
- hud._set_debug_cheats
|
||||
- gfx.particles.emit
|
||||
- gfx.particles.stop
|
||||
- gfx.particles.get_origin
|
||||
- gfx.particles.set_origin
|
||||
- assets.load_texture
|
||||
|
||||
Documented:
|
||||
- file.read_combined_list
|
||||
- cameras.get(int)
|
||||
- bjson.tobytes
|
||||
- bjson.frombytes
|
||||
- generation.create_fragment
|
||||
- generation.load_fragment
|
||||
- generation.save_fragment
|
||||
- generation.get_default_generator
|
||||
- generation.get_generators
|
||||
- uinode:getContentOffset
|
||||
- file.list
|
||||
- file.list_all_res
|
||||
- input.is_active
|
||||
- table.copy
|
||||
- table.count_pairs
|
||||
- table.random
|
||||
- table.has
|
||||
- table.index
|
||||
- table.remove_value
|
||||
- table.tostring
|
||||
- string.explode
|
||||
- string.split
|
||||
- string.pattern_safe
|
||||
- string.formatted_time
|
||||
- string.replace
|
||||
- string.trim
|
||||
- string.trim_left
|
||||
- string.trim_right
|
||||
- string.starts_with
|
||||
- string.ends_with
|
||||
- math.clamp
|
||||
- math.rand
|
||||
- is_array
|
||||
- parse_path
|
||||
- timeit
|
||||
- sleep
|
||||
|
||||
## Changes
|
||||
|
||||
- upgrade world regions format
|
||||
- upgrade toml parser to 1.0.0 support
|
||||
- json.tostring now accepts any supported value
|
||||
- json.parse now accepts any supported value as root element
|
||||
- major skybox optimization
|
||||
- chunks-renderer optimization
|
||||
- libspng replaced with libpng on Windows
|
||||
- console commands:
|
||||
- blocks.fill
|
||||
- fragment.save
|
||||
- added 'def' to core.get_setting_info tables
|
||||
- water texture
|
||||
|
||||
## Fixes
|
||||
|
||||
- [fix: extended block always main segment passed to on_iteract](https://github.com/MihailRis/VoxelEngine-Cpp/commit/fbca439b2da5a236a122c29488dc8809044ae919)
|
||||
- [fix: backlight setting not applying on change](https://github.com/MihailRis/VoxelEngine-Cpp/commit/d59fac61bb5ae5949b49f10ac71c22b595dcdff7 "fix: backlight setting not applying on change")
|
||||
- [fix: backlight not applied to entities](https://github.com/MihailRis/VoxelEngine-Cpp/commit/45a1e1df82967141dfb6d4b9b298deb4dfbf44c0 "fix: backlight not applied to entities")
|
||||
- [fix: extended block always main segment passed to on_iteract](https://github.com/MihailRis/VoxelEngine-Cpp/commit/fbca439b2da5a236a122c29488dc8809044ae919 "fix: extended block always main segment passed to on_iteract")
|
||||
- [fix block.get_hitbox with non rotatable blocks](https://github.com/MihailRis/VoxelEngine-Cpp/commit/b9074ebe4788d0016a9fd7563b59816b6300c06d "fix block.get_hitbox with non rotatable blocks")
|
||||
- [fix: entity shading is incorrect when it is upper than max height](https://github.com/MihailRis/VoxelEngine-Cpp/commit/45a793d6475b4d5b7c59e9c18492aa45767e2236 "fix: entity shading is incorrect when it is upper than max height")
|
||||
- [fix: toggle fullscreen GLFW invalid enum error](https://github.com/MihailRis/VoxelEngine-Cpp/commit/85bea6f17dc7815569a28e70423b704c476ed410 "fix: toggle fullscreen GLFW invalid enum error")
|
||||
- [fix: flight can stop on noclip enabled](https://github.com/MihailRis/VoxelEngine-Cpp/commit/f63ab345eaaf7885cfd0298a99cea58a423741fb "fix: flight can stop on noclip enabled")
|
||||
- [fix: block model "x" preview](https://github.com/MihailRis/VoxelEngine-Cpp/pull/300)
|
||||
- [Batch3D::point() buffer overflow](https://github.com/MihailRis/VoxelEngine-Cpp/pull/302)
|
||||
- [fix player entity teleport using debug_panel](https://github.com/MihailRis/VoxelEngine-Cpp/commit/ba9417a7e4638a3b09568895e4af5b702da80c16)
|
||||
- fix fatal animator error
|
||||
- [fix fatal error on editing texbox not having any consumer](https://github.com/MihailRis/VoxelEngine-Cpp/commit/22fa082fc6299ffa3196d62c67e01b849c35b8eb)
|
||||
- [fix commands boolean type support](https://github.com/MihailRis/VoxelEngine-Cpp/commit/a50cb109c8e3ca0f7a591bf126f07aee36c962e6)
|
||||
- [fix potential null dereferences on incorrect block.* functions use](https://github.com/MihailRis/VoxelEngine-Cpp/commit/961773c9f9745c15eb8d697c1538ac8e21f24da3)
|
||||
- [fix: draw-group not copied](https://github.com/MihailRis/VoxelEngine-Cpp/commit/dc8bad2af67e70b0b2346f516028e5795f597737)
|
||||
- [fix: generator-providing pack may be removed](https://github.com/MihailRis/VoxelEngine-Cpp/commit/6f2f365278eb1866c773890471b7269a5ef45305)
|
||||
- [fix colision check on block place](https://github.com/MihailRis/VoxelEngine-Cpp/commit/726ee8ad703bc57530b881450b8839aaec6b97c9)
|
||||
- [fix collision detection bug](https://github.com/MihailRis/VoxelEngine-Cpp/commit/7fcc34ba4cf14097dfda26054b028c5e8771d26c)
|
||||
- [fix: blocks lighting bug fix](https://github.com/MihailRis/VoxelEngine-Cpp/commit/9d3e872f88de2648f8c0f2e4611b30f5ce8999cf)
|
||||
- [fix: inaccurate framerate limit on Windows](https://github.com/MihailRis/VoxelEngine-Cpp/commit/3f531bbf98da5ad751dce1220c5c5fdf35f86c92)
|
||||
- [fix block.get_hitbox again](https://github.com/MihailRis/VoxelEngine-Cpp/commit/edad594101e5808ccf14e0edefedbe87cb8f983b)
|
||||
- [fix string.replace](https://github.com/MihailRis/VoxelEngine-Cpp/commit/44fd5416a9a110a12f8b3f2d369e5638055b306e)
|
||||
|
||||
@ -3,7 +3,7 @@
|
||||
## Latest release
|
||||
|
||||
- [Download](https://github.com/MihailRis/VoxelEngine-Cpp/releases/latest) | [Скачать](https://github.com/MihailRis/VoxelEngine-Cpp/releases/latest)
|
||||
- [Documentation](https://github.com/MihailRis/VoxelEngine-Cpp/blob/release-0.23/doc/en/main-page.md) | [Документация](https://github.com/MihailRis/VoxelEngine-Cpp/blob/release-0.23/doc/ru/main-page.md)
|
||||
- [Documentation](https://github.com/MihailRis/VoxelEngine-Cpp/blob/release-0.24/doc/en/main-page.md) | [Документация](https://github.com/MihailRis/VoxelEngine-Cpp/blob/release-0.24/doc/ru/main-page.md)
|
||||
|
||||
## Build project in Linux
|
||||
|
||||
|
||||
@ -1,8 +1,6 @@
|
||||
# Documentation
|
||||
|
||||
Documentation for the engine of in-development version 0.24.
|
||||
|
||||
[Documentation for the engine of stable version 0.23.x.](https://github.com/MihailRis/VoxelEngine-Cpp/blob/release-0.23/doc/en/main-page.md)
|
||||
Documentation for the engine of version 0.24.
|
||||
|
||||
## Sections
|
||||
|
||||
|
||||
@ -1,8 +1,6 @@
|
||||
# Документация
|
||||
|
||||
Документация движка разрабатываемой версии 0.24.
|
||||
|
||||
[Документация движка стабильной версии 0.23.x.](https://github.com/MihailRis/VoxelEngine-Cpp/blob/release-0.23/doc/ru/main-page.md)
|
||||
Документация движка версии 0.24.
|
||||
|
||||
## Разделы
|
||||
|
||||
|
||||
@ -60,13 +60,14 @@ rules.reset(name: str)
|
||||
## Стандартные правила
|
||||
|
||||
|
||||
| Имя | Описание | По-умолчанию |
|
||||
| -------------------- | --------------------------------------------------------------- | ------------ |
|
||||
| cheat-commands | Разрешить команды, имена которых есть в массиве console.cheats. | true |
|
||||
| allow-content-access | Разрешить панель доступа к контенту. | true |
|
||||
| allow-flight | Разрешить полёт | true |
|
||||
| allow-noclip | Разрешить включение noclip. | true |
|
||||
| allow-attack | Разрешить атаковать сущности. | true |
|
||||
| allow-destroy | Разрешить разрушение блоков. | true |
|
||||
| allow-cheat-movement | Разрешить специальные клавиши быстрого перемещения. | true |
|
||||
| allow-debug-cheats | Разрешить нечестные элементы управления на дебаг-панели. | true |
|
||||
| Имя | Описание | По-умолчанию |
|
||||
| ---------------------- | --------------------------------------------------------------- | ------------ |
|
||||
| cheat-commands | Разрешить команды, имена которых есть в массиве console.cheats. | true |
|
||||
| allow-content-access | Разрешить панель доступа к контенту. | true |
|
||||
| allow-flight | Разрешить полёт | true |
|
||||
| allow-noclip | Разрешить включение noclip. | true |
|
||||
| allow-attack | Разрешить атаковать сущности. | true |
|
||||
| allow-destroy | Разрешить разрушение блоков. | true |
|
||||
| allow-cheat-movement | Разрешить специальные клавиши быстрого перемещения. | true |
|
||||
| allow-debug-cheats | Разрешить нечестные элементы управления на дебаг-панели. | true |
|
||||
| allow-fast-interaction | Разрешить быстрое взаимодействие. | true |
|
||||
|
||||
@ -1,12 +1,19 @@
|
||||
history = session.get_entry("commands_history")
|
||||
history_pointer = #history
|
||||
|
||||
local warnings_all = {}
|
||||
|
||||
local warning_id = 0
|
||||
events.on("core:warning", function (wtype, text)
|
||||
local full = wtype..": "..text
|
||||
if table.has(warnings_all, full) then
|
||||
return
|
||||
end
|
||||
document.problemsLog:add(gui.template("problem", {
|
||||
type="warning", text=wtype..": "..text, id=tostring(warning_id)
|
||||
type="warning", text=full, id=tostring(warning_id)
|
||||
}))
|
||||
warning_id = warning_id + 1
|
||||
table.insert(warnings_all, full)
|
||||
end)
|
||||
|
||||
function setup_variables()
|
||||
|
||||
@ -263,6 +263,9 @@ function __vc_create_hud_rules()
|
||||
_rules.create("allow-cheat-movement", true, function(value)
|
||||
input.set_enabled("movement.cheat", value)
|
||||
end)
|
||||
_rules.create("allow-fast-interaction", true, function(value)
|
||||
input.set_enabled("player.fast_interaction", value)
|
||||
end)
|
||||
_rules.create("allow-debug-cheats", true, function(value)
|
||||
hud._set_debug_cheats(value)
|
||||
end)
|
||||
@ -291,6 +294,9 @@ function __vc_on_world_quit()
|
||||
_rules.clear()
|
||||
end
|
||||
|
||||
assets = {}
|
||||
assets.load_texture = core.__load_texture
|
||||
|
||||
-- --------- Deprecated functions ------ --
|
||||
local function wrap_deprecated(func, name, alternatives)
|
||||
return function (...)
|
||||
|
||||
@ -235,6 +235,16 @@ void AssetsLoader::addDefaults(AssetsLoader& loader, const Content* content) {
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const auto& [_, def] : content->blocks.getDefs()) {
|
||||
if (!def->modelName.empty() &&
|
||||
def->modelName.find(':') == std::string::npos) {
|
||||
loader.add(
|
||||
AssetType::MODEL,
|
||||
MODELS_FOLDER + "/" + def->modelName,
|
||||
def->modelName
|
||||
);
|
||||
}
|
||||
}
|
||||
for (const auto& [_, def] : content->items.getDefs()) {
|
||||
if (def->modelName.find(':') == std::string::npos) {
|
||||
loader.add(
|
||||
|
||||
@ -29,8 +29,22 @@ ContentGfxCache::ContentGfxCache(const Content* content, Assets* assets)
|
||||
}
|
||||
}
|
||||
if (def->model == BlockModel::custom) {
|
||||
models[def->rt.id] =
|
||||
assets->require<model::Model>(def->modelName);
|
||||
auto model = assets->require<model::Model>(def->modelName);
|
||||
// temporary dirty fix tbh
|
||||
if (def->modelName.find(':') == std::string::npos) {
|
||||
for (auto& mesh : model.meshes) {
|
||||
size_t pos = mesh.texture.find(':');
|
||||
if (pos == std::string::npos) {
|
||||
continue;
|
||||
}
|
||||
if (auto region = atlas->getIf(mesh.texture.substr(pos+1))) {
|
||||
for (auto& vertex : mesh.vertices) {
|
||||
vertex.uv = region->apply(vertex.uv);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
models[def->rt.id] = std::move(model);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -181,6 +181,49 @@ static int l_get_setting_info(lua::State* L) {
|
||||
throw std::runtime_error("unsupported setting type");
|
||||
}
|
||||
|
||||
#include "coders/png.hpp"
|
||||
#include "debug/Logger.hpp"
|
||||
#include "files/files.hpp"
|
||||
#include "graphics/core/Texture.hpp"
|
||||
|
||||
/// FIXME: replace with in-memory implementation
|
||||
|
||||
static void load_texture(
|
||||
const ubyte* bytes, size_t size, const std::string& destname
|
||||
) {
|
||||
auto path = engine->getPaths()->resolve("export:.__vc_imagedata");
|
||||
try {
|
||||
files::write_bytes(path, bytes, size);
|
||||
engine->getAssets()->store(png::load_texture(path.u8string()), destname);
|
||||
std::filesystem::remove(path);
|
||||
} catch (const std::runtime_error& err) {
|
||||
debug::Logger logger("lua.corelib");
|
||||
logger.error() << "could not to decode image: " << err.what();
|
||||
}
|
||||
}
|
||||
|
||||
static int l_load_texture(lua::State* L) {
|
||||
if (lua::istable(L, 1)) {
|
||||
lua::pushvalue(L, 1);
|
||||
size_t size = lua::objlen(L, 1);
|
||||
util::Buffer<ubyte> buffer(size);
|
||||
for (size_t i = 0; i < size; i++) {
|
||||
lua::rawgeti(L, i + 1);
|
||||
buffer[i] = lua::tointeger(L, -1);
|
||||
lua::pop(L);
|
||||
}
|
||||
lua::pop(L);
|
||||
load_texture(buffer.data(), buffer.size(), lua::require_string(L, 2));
|
||||
} else if (auto bytes = lua::touserdata<lua::LuaBytearray>(L, 1)) {
|
||||
load_texture(
|
||||
bytes->data().data(),
|
||||
bytes->data().size(),
|
||||
lua::require_string(L, 2)
|
||||
);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
#include "util/platform.hpp"
|
||||
|
||||
static int l_open_folder(lua::State* L) {
|
||||
@ -208,4 +251,6 @@ const luaL_Reg corelib[] = {
|
||||
{"get_setting_info", lua::wrap<l_get_setting_info>},
|
||||
{"open_folder", lua::wrap<l_open_folder>},
|
||||
{"quit", lua::wrap<l_quit>},
|
||||
{NULL, NULL}};
|
||||
{"__load_texture", lua::wrap<l_load_texture>},
|
||||
{NULL, NULL}
|
||||
};
|
||||
|
||||
@ -8,6 +8,7 @@
|
||||
#include "files/files.hpp"
|
||||
#include "util/stringutil.hpp"
|
||||
#include "api_lua.hpp"
|
||||
#include "../lua_engine.hpp"
|
||||
|
||||
namespace fs = std::filesystem;
|
||||
using namespace scripting;
|
||||
@ -47,17 +48,31 @@ static int l_read(lua::State* L) {
|
||||
);
|
||||
}
|
||||
|
||||
static std::set<std::string> writeable_entry_points {
|
||||
"world", "export", "config"
|
||||
};
|
||||
|
||||
static fs::path get_writeable_path(lua::State* L) {
|
||||
std::string rawpath = lua::require_string(L, 1);
|
||||
fs::path path = resolve_path(rawpath);
|
||||
auto entryPoint = rawpath.substr(0, rawpath.find(':'));
|
||||
if (writeable_entry_points.find(entryPoint) == writeable_entry_points.end()) {
|
||||
lua::emit_event(L, "core:warning", [=](auto L) {
|
||||
lua::pushstring(L, "writing to read-only entry point");
|
||||
lua::pushstring(L, entryPoint);
|
||||
return 2;
|
||||
});
|
||||
}
|
||||
return path;
|
||||
}
|
||||
|
||||
static int l_write(lua::State* L) {
|
||||
fs::path path = resolve_path(lua::require_string(L, 1));
|
||||
fs::path path = get_writeable_path(L);
|
||||
std::string text = lua::require_string(L, 2);
|
||||
files::write_string(path, text);
|
||||
return 1;
|
||||
}
|
||||
|
||||
static std::set<std::string> writeable_entry_points {
|
||||
"world", "export", "config"
|
||||
};
|
||||
|
||||
static int l_remove(lua::State* L) {
|
||||
std::string rawpath = lua::require_string(L, 1);
|
||||
fs::path path = resolve_path(rawpath);
|
||||
@ -155,15 +170,9 @@ static int read_bytes_from_table(
|
||||
}
|
||||
|
||||
static int l_write_bytes(lua::State* L) {
|
||||
int pathIndex = 1;
|
||||
fs::path path = get_writeable_path(L);
|
||||
|
||||
if (!lua::isstring(L, pathIndex)) {
|
||||
throw std::runtime_error("string expected");
|
||||
}
|
||||
|
||||
fs::path path = resolve_path(lua::require_string(L, pathIndex));
|
||||
|
||||
if (auto bytearray = lua::touserdata<lua::LuaBytearray>(L, -1)) {
|
||||
if (auto bytearray = lua::touserdata<lua::LuaBytearray>(L, 2)) {
|
||||
auto& bytes = bytearray->data();
|
||||
return lua::pushboolean(
|
||||
L, files::write_bytes(path, bytes.data(), bytes.size())
|
||||
|
||||
@ -25,6 +25,7 @@ static int l_tobytes(lua::State* L) {
|
||||
|
||||
static int l_tostring(lua::State* L) {
|
||||
if (lua::istable(L, 1)) {
|
||||
lua::pushvalue(L, 1);
|
||||
size_t size = lua::objlen(L, 1);
|
||||
util::Buffer<char> buffer(size);
|
||||
for (size_t i = 0; i < size; i++) {
|
||||
@ -32,6 +33,7 @@ static int l_tostring(lua::State* L) {
|
||||
buffer[i] = lua::tointeger(L, -1);
|
||||
lua::pop(L);
|
||||
}
|
||||
lua::pop(L);
|
||||
return lua::pushlstring(L, buffer.data(), size);
|
||||
} else if (auto bytes = lua::touserdata<lua::LuaBytearray>(L, 1)) {
|
||||
return lua::pushstring(
|
||||
|
||||
@ -38,6 +38,6 @@ struct UVRegion {
|
||||
inline glm::vec2 apply(const glm::vec2& uv) {
|
||||
float w = getWidth();
|
||||
float h = getHeight();
|
||||
return glm::vec2(u1 + uv.x / w, v1 + uv.y / h);
|
||||
return glm::vec2(u1 + uv.x * w, v1 + uv.y * h);
|
||||
}
|
||||
};
|
||||
|
||||
@ -82,10 +82,12 @@ void platform::open_folder(const std::filesystem::path& folder) {
|
||||
}
|
||||
#ifdef __APPLE__
|
||||
auto cmd = "open " + util::quote(folder.u8string());
|
||||
system(cmd.c_str());
|
||||
#elif defined(_WIN32)
|
||||
auto cmd = "start explorer " + util::quote(folder.u8string());
|
||||
ShellExecuteW(NULL, L"open", folder.wstring().c_str(), NULL, NULL, SW_SHOWDEFAULT);
|
||||
#else
|
||||
auto cmd = "xdg-open " + util::quote(folder.u8string());
|
||||
#endif
|
||||
system(cmd.c_str());
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user