mirror of
https://github.com/Refactorio/RedMew.git
synced 2025-01-03 22:52:13 +02:00
Add player settings for corpse pings + tests.
This commit is contained in:
parent
86cbfac9a7
commit
eb7b43cf1c
@ -3,6 +3,18 @@ local Global = require 'utils.global'
|
||||
local Task = require 'utils.task'
|
||||
local Token = require 'utils.token'
|
||||
local Utils = require 'utils.core'
|
||||
local Settings = require 'utils.redmew_settings'
|
||||
|
||||
local Public = {}
|
||||
|
||||
local ping_own_death_name = 'corpse_util.ping_own_death'
|
||||
local ping_other_death_name = 'corpse_util.ping_other_death'
|
||||
|
||||
Public.ping_own_death_name = ping_own_death_name
|
||||
Public.ping_other_death_name = ping_other_death_name
|
||||
|
||||
Settings.register(ping_own_death_name, Settings.types.boolean, true, 'corpse_util.ping_own_death')
|
||||
Settings.register(ping_other_death_name, Settings.types.boolean, false, 'corpse_util.ping_other_death')
|
||||
|
||||
local player_corpses = {}
|
||||
|
||||
@ -49,13 +61,26 @@ local function player_died(event)
|
||||
return
|
||||
end
|
||||
|
||||
player.force.print({
|
||||
'corpse_util.marked_tag',
|
||||
player.name,
|
||||
string.format('%.1f', position.x),
|
||||
string.format('%.1f', position.y),
|
||||
player.surface.name
|
||||
})
|
||||
if Settings.get(player_index, ping_own_death_name) then
|
||||
player.print({
|
||||
'corpse_util.own_corpse_location',
|
||||
string.format('%.1f', position.x),
|
||||
string.format('%.1f', position.y),
|
||||
player.surface.name
|
||||
})
|
||||
end
|
||||
|
||||
for _, other_player in pairs(player.force.players) do
|
||||
if other_player ~= player and Settings.get(other_player.index, ping_other_death_name) then
|
||||
other_player.print({
|
||||
'corpse_util.other_corpse_location',
|
||||
player.name,
|
||||
string.format('%.1f', position.x),
|
||||
string.format('%.1f', position.y),
|
||||
player.surface.name
|
||||
})
|
||||
end
|
||||
end
|
||||
|
||||
player_corpses[player_index * 0x100000000 + tick] = tag
|
||||
end
|
||||
@ -144,3 +169,11 @@ Event.add(defines.events.on_player_died, player_died)
|
||||
Event.add(defines.events.on_character_corpse_expired, corpse_expired)
|
||||
Event.add(defines.events.on_pre_player_mined_item, mined_entity)
|
||||
Event.add(defines.events.on_gui_opened, on_gui_opened)
|
||||
|
||||
function Public.clear()
|
||||
table.clear_table(player_corpses)
|
||||
end
|
||||
|
||||
Public._player_died = player_died
|
||||
|
||||
return Public
|
||||
|
235
features/corpse_util_tests.lua
Normal file
235
features/corpse_util_tests.lua
Normal file
@ -0,0 +1,235 @@
|
||||
local Declare = require 'utils.test.declare'
|
||||
local EventFactory = require 'utils.test.event_factory'
|
||||
local Assert = require 'utils.test.assert'
|
||||
local Helper = require 'utils.test.helper'
|
||||
local Settings = require 'utils.redmew_settings'
|
||||
local CorpseUtil = require 'features.corpse_util'
|
||||
|
||||
local function test_teardown(context)
|
||||
context:add_teardown(CorpseUtil.clear)
|
||||
end
|
||||
|
||||
local function declare_test(name, func)
|
||||
local function test_func(context)
|
||||
test_teardown(context)
|
||||
func(context)
|
||||
end
|
||||
|
||||
Declare.test(name, test_func)
|
||||
end
|
||||
|
||||
Declare.module({'features', 'corpse_util'}, function()
|
||||
local teardown
|
||||
|
||||
Declare.module_startup(function(context)
|
||||
teardown = Helper.startup_test_surface(context)
|
||||
|
||||
-- wait for surface to be charted, needed before a tag can be created.
|
||||
context:next(function()
|
||||
local player = context.player
|
||||
Helper.wait_for_chunk_to_be_charted(context, player.force, player.surface, {0, 0})
|
||||
end)
|
||||
end)
|
||||
|
||||
Declare.module_teardown(function()
|
||||
teardown()
|
||||
end)
|
||||
|
||||
local function change_settings_for_test(context, key, value)
|
||||
local player_index = context.player.index
|
||||
|
||||
local current_value = Settings.get(player_index, key)
|
||||
Settings.set(player_index, key, value)
|
||||
|
||||
context:add_teardown(function()
|
||||
Settings.set(player_index, key, current_value)
|
||||
end)
|
||||
end
|
||||
|
||||
local function fake_death(player)
|
||||
local surface = player.surface
|
||||
local position = player.position
|
||||
|
||||
local entity = surface.create_entity {
|
||||
name = 'character-corpse',
|
||||
position = position,
|
||||
player_index = player.index
|
||||
}
|
||||
|
||||
if not entity or not entity.valid then
|
||||
error('no corpse')
|
||||
end
|
||||
|
||||
return EventFactory.on_player_died(player.index)
|
||||
end
|
||||
|
||||
declare_test('ping player corpse location when died', function(context)
|
||||
-- Arrange.
|
||||
local player = context.player
|
||||
|
||||
local actual_text
|
||||
|
||||
Helper.modify_lua_object(context, player, 'print', function(text)
|
||||
actual_text = text
|
||||
end)
|
||||
|
||||
Helper.modify_lua_object(context, game, 'get_player', function()
|
||||
return player
|
||||
end)
|
||||
|
||||
local event = fake_death(player)
|
||||
|
||||
-- Act.
|
||||
CorpseUtil._player_died(event)
|
||||
|
||||
-- Assert.
|
||||
local expected = {'corpse_util.own_corpse_location', '0.0', '0.0', player.surface.name}
|
||||
Assert.table_equal(expected, actual_text)
|
||||
end)
|
||||
|
||||
declare_test('ping other player corpse location when other player died', function(context)
|
||||
-- Arrange.
|
||||
local player = context.player
|
||||
local force = player.force
|
||||
|
||||
local actual_text
|
||||
|
||||
local second_player = {
|
||||
index = 2,
|
||||
valid = true,
|
||||
name = 'second_player',
|
||||
surface = player.surface,
|
||||
force = force,
|
||||
print = function()
|
||||
end,
|
||||
position = EventFactory.position({1, 1})
|
||||
}
|
||||
|
||||
change_settings_for_test(context, CorpseUtil.ping_other_death_name, true)
|
||||
|
||||
Helper.modify_lua_object(context, game, 'get_player', function(index)
|
||||
if index == player.index then
|
||||
return player
|
||||
end
|
||||
|
||||
if index == second_player.index then
|
||||
return second_player
|
||||
end
|
||||
end)
|
||||
|
||||
Helper.modify_lua_object(context, force, 'players', {player, second_player})
|
||||
|
||||
Helper.modify_lua_object(context, player, 'print', function(text)
|
||||
actual_text = text
|
||||
end)
|
||||
|
||||
local event = fake_death(second_player)
|
||||
|
||||
-- Act.
|
||||
CorpseUtil._player_died(event)
|
||||
|
||||
-- Assert.
|
||||
local expected = {'corpse_util.other_corpse_location', second_player.name, '1.0', '1.0', player.surface.name}
|
||||
Assert.table_equal(expected, actual_text)
|
||||
end)
|
||||
|
||||
declare_test('do not ping player corpse location when died and setting disabled', function(context)
|
||||
-- Arrange.
|
||||
local player = context.player
|
||||
change_settings_for_test(context, CorpseUtil.ping_own_death_name, false)
|
||||
|
||||
local actual_text
|
||||
|
||||
Helper.modify_lua_object(context, player, 'print', function(text)
|
||||
actual_text = text
|
||||
end)
|
||||
|
||||
Helper.modify_lua_object(context, game, 'get_player', function()
|
||||
return player
|
||||
end)
|
||||
|
||||
local event = fake_death(player)
|
||||
|
||||
-- Act.
|
||||
CorpseUtil._player_died(event)
|
||||
|
||||
-- Assert.
|
||||
Assert.is_nil(actual_text)
|
||||
end)
|
||||
|
||||
declare_test('do not ping other player corpse location when other player died and settings disabled',
|
||||
function(context)
|
||||
-- Arrange.
|
||||
local player = context.player
|
||||
local force = player.force
|
||||
|
||||
local actual_text
|
||||
|
||||
local second_player = {
|
||||
index = 2,
|
||||
valid = true,
|
||||
name = 'second_player',
|
||||
surface = player.surface,
|
||||
force = force,
|
||||
print = function()
|
||||
end,
|
||||
position = EventFactory.position({1, 1})
|
||||
}
|
||||
|
||||
change_settings_for_test(context, CorpseUtil.ping_other_death_name, false)
|
||||
|
||||
Helper.modify_lua_object(context, game, 'get_player', function(index)
|
||||
if index == player.index then
|
||||
return player
|
||||
end
|
||||
|
||||
if index == second_player.index then
|
||||
return second_player
|
||||
end
|
||||
end)
|
||||
|
||||
Helper.modify_lua_object(context, force, 'players', {player, second_player})
|
||||
|
||||
Helper.modify_lua_object(context, player, 'print', function(text)
|
||||
actual_text = text
|
||||
end)
|
||||
|
||||
local event = fake_death(second_player)
|
||||
|
||||
-- Act.
|
||||
CorpseUtil._player_died(event)
|
||||
|
||||
-- Assert.
|
||||
Assert.is_nil(actual_text)
|
||||
end)
|
||||
|
||||
declare_test('do not ping other player corpse location for self', function(context)
|
||||
-- Arrange.
|
||||
local player = context.player
|
||||
local force = player.force
|
||||
|
||||
local actual_text
|
||||
|
||||
change_settings_for_test(context, CorpseUtil.ping_own_death_name, false)
|
||||
change_settings_for_test(context, CorpseUtil.ping_other_death_name, true)
|
||||
|
||||
Helper.modify_lua_object(context, game, 'get_player', function()
|
||||
return player
|
||||
end)
|
||||
|
||||
Helper.modify_lua_object(context, player, 'force', force)
|
||||
Helper.modify_lua_object(context, force, 'players', {player})
|
||||
|
||||
Helper.modify_lua_object(context, player, 'print', function(text)
|
||||
actual_text = text
|
||||
end)
|
||||
|
||||
local event = fake_death(player)
|
||||
|
||||
-- Act.
|
||||
CorpseUtil._player_died(event)
|
||||
|
||||
-- Assert.
|
||||
Assert.is_nil(actual_text)
|
||||
end)
|
||||
end)
|
@ -179,4 +179,7 @@ instructions=Select a brush tile to replace [item=refined-concrete] and [item=re
|
||||
no_place_landfill=Coloured concrete can not be placed on landfill tiles.
|
||||
|
||||
[corpse_util]
|
||||
marked_tag=__1__'s corpse has been marked on the map at [gps=__2__,__3__,__4__]
|
||||
ping_own_death=Ping the location when you die.
|
||||
ping_other_death=Ping the location when other players die.
|
||||
own_corpse_location=[color=red][Corpse][/color] Your corpse is located at [gps=__1__,__2__,__3__]
|
||||
other_corpse_location=[Color=red][Corpse][/Color] __1__'s corpse is located at [gps=__2__,__3__,__4__]
|
||||
|
@ -1,3 +1,4 @@
|
||||
local table = require 'utils.table'
|
||||
local error = error
|
||||
local concat = table.concat
|
||||
|
||||
@ -25,6 +26,28 @@ function Public.equal(a, b, optional_message)
|
||||
error(message, 2)
|
||||
end
|
||||
|
||||
function Public.is_nil(value, optional_message)
|
||||
if value == nil then
|
||||
return
|
||||
end
|
||||
|
||||
local message = {tostring(value), ' was not nil'}
|
||||
if optional_message then
|
||||
message[#message + 1] = ' - '
|
||||
message[#message + 1] = optional_message
|
||||
end
|
||||
|
||||
message = concat(message)
|
||||
error(message, 2)
|
||||
end
|
||||
|
||||
function Public.table_equal(a, b)
|
||||
-- Todo write own table equal
|
||||
if not table.equals(a, b) then
|
||||
error('tables not equal', 2)
|
||||
end
|
||||
end
|
||||
|
||||
function Public.is_true(condition, optional_message)
|
||||
if not condition then
|
||||
error(optional_message or 'condition was not true', 2)
|
||||
|
@ -2,7 +2,7 @@ local Public = {}
|
||||
Public.__index = Public
|
||||
|
||||
function Public.new(player)
|
||||
return setmetatable({player = player, _steps = {}}, Public)
|
||||
return setmetatable({player = player, _steps = {}, _teardowns = {}}, Public)
|
||||
end
|
||||
|
||||
function Public.timeout(self, delay, func)
|
||||
@ -15,4 +15,15 @@ function Public.next(self, func)
|
||||
return self:timeout(1, func)
|
||||
end
|
||||
|
||||
function Public.wait(self, delay)
|
||||
return self:timeout(delay, function()
|
||||
end)
|
||||
end
|
||||
|
||||
function Public.add_teardown(self, func)
|
||||
local teardowns = self._teardowns
|
||||
teardowns[#teardowns + 1] = func
|
||||
return self
|
||||
end
|
||||
|
||||
return Public
|
||||
|
@ -57,18 +57,20 @@ function Public.on_player_deconstructed_area(player_index, surface, area, item)
|
||||
end
|
||||
|
||||
function Public.do_player_deconstruct_area(cursor, player, area, optional_skip_fog_of_war)
|
||||
cursor.deconstruct_area(
|
||||
{
|
||||
surface = player.surface,
|
||||
force = player.force,
|
||||
area = area,
|
||||
by_player = player,
|
||||
skip_fog_of_war = optional_skip_fog_of_war
|
||||
}
|
||||
)
|
||||
cursor.deconstruct_area({
|
||||
surface = player.surface,
|
||||
force = player.force,
|
||||
area = area,
|
||||
by_player = player,
|
||||
skip_fog_of_war = optional_skip_fog_of_war
|
||||
})
|
||||
|
||||
local event = Public.on_player_deconstructed_area(player.index, player.surface, area, cursor.name)
|
||||
Public.raise(event)
|
||||
end
|
||||
|
||||
function Public.on_player_died(player_index)
|
||||
return {name = defines.events.on_player_died, tick = game.tick, player_index = player_index, cause = nils}
|
||||
end
|
||||
|
||||
return Public
|
||||
|
@ -4,12 +4,9 @@ local Public = {}
|
||||
|
||||
local surface_count = 0
|
||||
|
||||
Global.register(
|
||||
{surface_count = surface_count},
|
||||
function(tbl)
|
||||
surface_count = tbl.surface_count
|
||||
end
|
||||
)
|
||||
Global.register({surface_count = surface_count}, function(tbl)
|
||||
surface_count = tbl.surface_count
|
||||
end)
|
||||
|
||||
local function get_surface_name()
|
||||
surface_count = surface_count + 1
|
||||
@ -17,62 +14,21 @@ local function get_surface_name()
|
||||
end
|
||||
|
||||
local autoplace_settings = {
|
||||
tile = {
|
||||
treat_missing_as_default = false,
|
||||
settings = {
|
||||
['grass-1'] = {frequency = 1, size = 1, richness = 1}
|
||||
}
|
||||
}
|
||||
tile = {treat_missing_as_default = false, settings = {['grass-1'] = {frequency = 1, size = 1, richness = 1}}}
|
||||
}
|
||||
|
||||
local autoplace_controls = {
|
||||
trees = {
|
||||
frequency = 1,
|
||||
richness = 1,
|
||||
size = 0
|
||||
},
|
||||
['enemy-base'] = {
|
||||
frequency = 1,
|
||||
richness = 1,
|
||||
size = 0
|
||||
},
|
||||
coal = {
|
||||
frequency = 1,
|
||||
richness = 1,
|
||||
size = 0
|
||||
},
|
||||
['copper-ore'] = {
|
||||
frequency = 1,
|
||||
richness = 1,
|
||||
size = 0
|
||||
},
|
||||
['crude-oil'] = {
|
||||
frequency = 1,
|
||||
richness = 1,
|
||||
size = 0
|
||||
},
|
||||
['iron-ore'] = {
|
||||
frequency = 1,
|
||||
richness = 1,
|
||||
size = 0
|
||||
},
|
||||
stone = {
|
||||
frequency = 1,
|
||||
richness = 1,
|
||||
size = 0
|
||||
},
|
||||
['uranium-ore'] = {
|
||||
frequency = 1,
|
||||
richness = 1,
|
||||
size = 0
|
||||
}
|
||||
trees = {frequency = 1, richness = 1, size = 0},
|
||||
['enemy-base'] = {frequency = 1, richness = 1, size = 0},
|
||||
coal = {frequency = 1, richness = 1, size = 0},
|
||||
['copper-ore'] = {frequency = 1, richness = 1, size = 0},
|
||||
['crude-oil'] = {frequency = 1, richness = 1, size = 0},
|
||||
['iron-ore'] = {frequency = 1, richness = 1, size = 0},
|
||||
stone = {frequency = 1, richness = 1, size = 0},
|
||||
['uranium-ore'] = {frequency = 1, richness = 1, size = 0}
|
||||
}
|
||||
|
||||
local cliff_settings = {
|
||||
cliff_elevation_0 = 1024,
|
||||
cliff_elevation_interval = 10,
|
||||
name = 'cliff'
|
||||
}
|
||||
local cliff_settings = {cliff_elevation_0 = 1024, cliff_elevation_interval = 10, name = 'cliff'}
|
||||
|
||||
function Public.startup_test_surface(context, options)
|
||||
options = options or {}
|
||||
@ -84,34 +40,29 @@ function Public.startup_test_surface(context, options)
|
||||
local old_position = player.position
|
||||
local old_character = player.character
|
||||
|
||||
local surface =
|
||||
game.create_surface(
|
||||
name,
|
||||
{
|
||||
width = area.x or area[1],
|
||||
height = area.y or area[2],
|
||||
autoplace_settings = autoplace_settings,
|
||||
autoplace_controls = autoplace_controls,
|
||||
cliff_settings = cliff_settings
|
||||
}
|
||||
)
|
||||
local surface = game.create_surface(name, {
|
||||
width = area.x or area[1],
|
||||
height = area.y or area[2],
|
||||
autoplace_settings = autoplace_settings,
|
||||
autoplace_controls = autoplace_controls,
|
||||
cliff_settings = cliff_settings
|
||||
})
|
||||
|
||||
surface.request_to_generate_chunks({0, 0}, 32)
|
||||
surface.force_generate_chunk_requests()
|
||||
player.force.chart_all(surface)
|
||||
|
||||
context:next(
|
||||
function()
|
||||
for k, v in pairs(surface.find_entities()) do
|
||||
v.destroy()
|
||||
end
|
||||
|
||||
surface.destroy_decoratives {area = {{-32, -32}, {32, 32}}}
|
||||
|
||||
player.character = nil
|
||||
player.teleport({0, 0}, surface)
|
||||
player.create_character()
|
||||
context:next(function()
|
||||
for k, v in pairs(surface.find_entities()) do
|
||||
v.destroy()
|
||||
end
|
||||
)
|
||||
|
||||
surface.destroy_decoratives {area = {{-32, -32}, {32, 32}}}
|
||||
|
||||
player.character = nil
|
||||
player.teleport({0, 0}, surface)
|
||||
player.create_character()
|
||||
end)
|
||||
|
||||
return function()
|
||||
player.character = nil
|
||||
@ -125,4 +76,26 @@ function Public.startup_test_surface(context, options)
|
||||
end
|
||||
end
|
||||
|
||||
function Public.wait_for_chunk_to_be_charted(context, force, surface, chunk_position, next)
|
||||
if not force.is_chunk_charted(surface, chunk_position) then
|
||||
context:next(function()
|
||||
Public.wait_for_chunk_to_be_charted(context, force, surface, chunk_position)
|
||||
end)
|
||||
return
|
||||
end
|
||||
|
||||
if next then
|
||||
context:next(next)
|
||||
end
|
||||
end
|
||||
|
||||
function Public.modify_lua_object(context, object, key, value)
|
||||
local old_value = object[key]
|
||||
rawset(object, key, value)
|
||||
|
||||
context:add_teardown(function()
|
||||
rawset(object, key, old_value)
|
||||
end)
|
||||
end
|
||||
|
||||
return Public
|
||||
|
@ -8,9 +8,7 @@ local pcall = pcall
|
||||
|
||||
local Public = {}
|
||||
|
||||
Public.events = {
|
||||
tests_run_finished = Event.generate_event_name('test_run_finished')
|
||||
}
|
||||
Public.events = {tests_run_finished = Event.generate_event_name('test_run_finished')}
|
||||
|
||||
local run_runnables_token
|
||||
|
||||
@ -67,6 +65,10 @@ local function print_hook_error(hook)
|
||||
hook.context.player.print(table.concat {'Failed ', hook.name, " hook -':", tostring(hook.error)}, {r = 1})
|
||||
end
|
||||
|
||||
local function print_teardown_error(context, name, error_message)
|
||||
context.player.print(table.concat {'Failed ', name, " teardown -':", error_message}, {r = 1})
|
||||
end
|
||||
|
||||
local function record_hook_error_in_module(hook)
|
||||
if hook.name == 'startup' then
|
||||
hook.module.startup_error = hook.error
|
||||
@ -86,6 +88,32 @@ local function do_termination(data)
|
||||
return true
|
||||
end
|
||||
|
||||
local function run_teardown(teardown, errors)
|
||||
local success, error_message = pcall(teardown)
|
||||
|
||||
if not success then
|
||||
errors[#errors + 1] = error_message
|
||||
end
|
||||
end
|
||||
|
||||
local function do_teardowns(context, name)
|
||||
local teardowns = context._teardowns
|
||||
local errors = {}
|
||||
|
||||
for i = 1, #teardowns do
|
||||
run_teardown(teardowns[i], errors)
|
||||
end
|
||||
|
||||
if #errors > 0 then
|
||||
local error_message = table.concat(errors, '\n')
|
||||
print_teardown_error(context, name, error_message)
|
||||
|
||||
return error_message
|
||||
end
|
||||
|
||||
return nil
|
||||
end
|
||||
|
||||
local function run_hook(hook)
|
||||
local context = hook.context
|
||||
local steps = context._steps
|
||||
@ -104,14 +132,22 @@ local function run_hook(hook)
|
||||
hook.error = return_value
|
||||
print_hook_error(hook)
|
||||
record_hook_error_in_module(hook)
|
||||
do_teardowns(context, hook.name)
|
||||
return false
|
||||
end
|
||||
|
||||
if current_step == #steps then
|
||||
return true
|
||||
if current_step ~= #steps then
|
||||
return nil
|
||||
end
|
||||
|
||||
return nil
|
||||
local error_message = do_teardowns(context, hook.name)
|
||||
if error_message then
|
||||
hook.error = error_message
|
||||
record_hook_error_in_module(hook)
|
||||
return false
|
||||
end
|
||||
|
||||
return true
|
||||
end
|
||||
|
||||
local function do_hook(hook, data)
|
||||
@ -152,16 +188,24 @@ local function run_test(test)
|
||||
print_error(context.player, test.name, return_value)
|
||||
test.passed = false
|
||||
test.error = return_value
|
||||
do_teardowns(context, test.name)
|
||||
return false
|
||||
end
|
||||
|
||||
if current_step == #steps then
|
||||
print_success(context.player, test.name)
|
||||
test.passed = true
|
||||
return true
|
||||
if current_step ~= #steps then
|
||||
return nil
|
||||
end
|
||||
|
||||
return nil
|
||||
local error_message = do_teardowns(context, test.name)
|
||||
if error_message then
|
||||
test.passed = false
|
||||
test.error = error_message
|
||||
return false
|
||||
end
|
||||
|
||||
print_success(context.player, test.name)
|
||||
test.passed = true
|
||||
return true
|
||||
end
|
||||
|
||||
local function do_test(test, data)
|
||||
@ -219,16 +263,14 @@ end
|
||||
|
||||
local function run(runnables, player, options)
|
||||
options = validate_options(options)
|
||||
run_runnables(
|
||||
{
|
||||
runnables = runnables,
|
||||
player = player,
|
||||
index = 1,
|
||||
count = 0,
|
||||
fail_count = 0,
|
||||
stop_on_first_error = options.stop_on_first_error
|
||||
}
|
||||
)
|
||||
run_runnables({
|
||||
runnables = runnables,
|
||||
player = player,
|
||||
index = 1,
|
||||
count = 0,
|
||||
fail_count = 0,
|
||||
stop_on_first_error = options.stop_on_first_error
|
||||
})
|
||||
end
|
||||
|
||||
function Public.run_module(module, player, options)
|
||||
|
Loading…
Reference in New Issue
Block a user