mirror of
https://gitlab.com/veloren/veloren.git
synced 2024-08-30 18:12:32 +00:00
Merge branch 'zesterer/econ-tweaks' into 'master'
Rebalanced economy somewhat See merge request veloren/veloren!2148
This commit is contained in:
commit
cb817c0313
@ -2,44 +2,45 @@
|
||||
loot_tables: [
|
||||
// balance the loot tables against each other (higher= more common= smaller price)
|
||||
// Weapons
|
||||
(16.0, "common.loot_tables.weapons.starter"),
|
||||
(8.0, "common.loot_tables.weapons.tier-0"),
|
||||
(4.0, "common.loot_tables.weapons.tier-1"),
|
||||
(2.0, "common.loot_tables.weapons.tier-2"),
|
||||
(1.0, "common.loot_tables.weapons.tier-3"),
|
||||
(0.5, "common.loot_tables.weapons.tier-4"),
|
||||
(0.25, "common.loot_tables.weapons.tier-5"),
|
||||
(0.125, "common.loot_tables.weapons.cultist"),
|
||||
(0.125, "common.loot_tables.weapons.cave"),
|
||||
(0.0625, "common.loot_tables.weapons.legendary"),
|
||||
(16.0, true, "common.loot_tables.weapons.starter"),
|
||||
(12.0, true, "common.loot_tables.weapons.tier-0"),
|
||||
(6.0, true, "common.loot_tables.weapons.tier-1"),
|
||||
(4.0, true, "common.loot_tables.weapons.tier-2"),
|
||||
(2.0, true, "common.loot_tables.weapons.tier-3"),
|
||||
(1.0, false, "common.loot_tables.weapons.tier-4"),
|
||||
(0.5, false, "common.loot_tables.weapons.tier-5"),
|
||||
(0.05, false, "common.loot_tables.weapons.cultist"),
|
||||
(0.125, false, "common.loot_tables.weapons.cave"),
|
||||
(0.0625, false, "common.loot_tables.weapons.legendary"),
|
||||
// Armor
|
||||
(20.0, "common.loot_tables.armor.cloth"),
|
||||
(6.0, "common.loot_tables.armor.agile"),
|
||||
(3.0, "common.loot_tables.armor.swift"),
|
||||
(6.0, "common.loot_tables.armor.druid"),
|
||||
(2.0, "common.loot_tables.armor.twigs"),
|
||||
(2.0, "common.loot_tables.armor.twigsflowers"),
|
||||
(2.0, "common.loot_tables.armor.twigsleaves"),
|
||||
(0.5, "common.loot_tables.armor.plate"),
|
||||
(0.25, "common.loot_tables.armor.steel"),
|
||||
(0.125, "common.loot_tables.armor.cultist"),
|
||||
(20.0, true, "common.loot_tables.armor.cloth"),
|
||||
(6.0, true, "common.loot_tables.armor.agile"),
|
||||
(3.0, true, "common.loot_tables.armor.swift"),
|
||||
(6.0, true, "common.loot_tables.armor.druid"),
|
||||
(1.0, true, "common.loot_tables.armor.twigs"),
|
||||
(1.0, true, "common.loot_tables.armor.twigsflowers"),
|
||||
(1.0, true, "common.loot_tables.armor.twigsleaves"),
|
||||
(0.5, true, "common.loot_tables.armor.plate"),
|
||||
(0.25, false, "common.loot_tables.armor.steel"),
|
||||
(0.075, false, "common.loot_tables.armor.cultist"),
|
||||
// Materials
|
||||
(7.5, "common.loot_tables.materials.common"),
|
||||
(5.0, "common.loot_tables.materials.underground"),
|
||||
(7.5, true, "common.loot_tables.materials.common"),
|
||||
(8.0, true, "common.loot_tables.materials.underground"),
|
||||
// Food
|
||||
(0.5, "common.loot_tables.food.farm_ingredients"),
|
||||
(0.25, "common.loot_tables.food.wild_ingredients"),
|
||||
(0.1, "common.loot_tables.food.prepared"),
|
||||
(0.3, true, "common.loot_tables.food.farm_ingredients"),
|
||||
(0.4, true, "common.loot_tables.food.wild_ingredients"),
|
||||
(0.2, true, "common.loot_tables.food.prepared"),
|
||||
// TODO: Change consumables when they are split up later
|
||||
(1.0, "common.loot_tables.consumables"),
|
||||
],
|
||||
(1.0, true, "common.loot_tables.consumables"),
|
||||
(0.5, false, "common.loot_tables.trading"),
|
||||
],
|
||||
// this is the amount of that good the most common item represents
|
||||
// so basically this table balances the goods against each other (higher=less valuable)
|
||||
good_scaling: [
|
||||
(Potions, 0.001), // common.items.consumable.potion_minor
|
||||
(Food, 2.0), // common.items.food.mushroom
|
||||
(Coin, 10.0), // common.items.utility.coins
|
||||
(Armor, 0.2), // common.items.armor.misc.pants.worker_blue
|
||||
(Tools, 0.1), // common.items.weapons.staff.starter_staff
|
||||
(Ingredients, 1.0), // common.items.crafting_ing.leather_scraps
|
||||
(Potions, 0.0075), // common.items.consumable.potion_minor
|
||||
(Food, 0.1), // common.items.food.mushroom
|
||||
(Coin, 1.0), // common.items.utility.coins
|
||||
(Armor, 0.05), // common.items.armor.misc.pants.worker_blue
|
||||
(Tools, 0.05), // common.items.weapons.staff.starter_staff
|
||||
(Ingredients, 0.25), // common.items.crafting_ing.leather_scraps
|
||||
])
|
||||
|
@ -1,7 +1,6 @@
|
||||
[
|
||||
(1.0, Item("common.items.food.apple")),
|
||||
(1.0, Item("common.items.food.carrot")),
|
||||
(1.0, Item("common.items.food.cheese")),
|
||||
(1.0, Item("common.items.food.lettuce")),
|
||||
(1.0, Item("common.items.food.tomato")),
|
||||
]
|
||||
]
|
||||
|
@ -1,9 +1,9 @@
|
||||
[
|
||||
(0.2, Item("common.items.food.apple_mushroom_curry")),
|
||||
(0.4, Item("common.items.food.apple_mushroom_curry")),
|
||||
(1.0, Item("common.items.food.apple_stick")),
|
||||
(2.0, Item("common.items.food.cheese")),
|
||||
(1.0, Item("common.items.food.mushroom_stick")),
|
||||
(1.0, Item("common.items.food.plainsalad")),
|
||||
(1.0, Item("common.items.food.sunflower_icetea")),
|
||||
(1.0, Item("common.items.food.cheese")),
|
||||
(1.1, Item("common.items.food.mushroom_stick")),
|
||||
(1.2, Item("common.items.food.plainsalad")),
|
||||
(0.5, Item("common.items.food.sunflower_icetea")),
|
||||
(1.0, Item("common.items.food.tomatosalad")),
|
||||
]
|
||||
]
|
||||
|
@ -1,6 +1,6 @@
|
||||
[
|
||||
(1.0, Item("common.items.food.apple")),
|
||||
(1.0, Item("common.items.food.cheese")),
|
||||
(0.3, Item("common.items.food.cheese")),
|
||||
(1.0, Item("common.items.food.coconut")),
|
||||
(1.0, Item("common.items.food.mushroom")),
|
||||
]
|
||||
(1.5, Item("common.items.food.mushroom")),
|
||||
]
|
||||
|
5
assets/common/loot_tables/trading.ron
Normal file
5
assets/common/loot_tables/trading.ron
Normal file
@ -0,0 +1,5 @@
|
||||
// Loot table that exists purely for price rationalisation
|
||||
[
|
||||
(1.0, Item("common.items.crafting_ing.honey")),
|
||||
(0.5, Item("common.items.crafting_ing.icy_fang")),
|
||||
]
|
@ -1,5 +1,10 @@
|
||||
// we use a vector to easily generate a key into all the economic data containers
|
||||
([
|
||||
(
|
||||
name: "Banker",
|
||||
orders: [ (Ingredients, 12.0), (Stone, 4.0), (Tools, 1.0), (RoadSecurity, 4.0) ],
|
||||
products: [ (Coin, 16.0) ],
|
||||
),
|
||||
(
|
||||
name: "Cook",
|
||||
orders: [ (Flour, 12.0), (Meat, 4.0), (Wood, 1.5), (Stone, 1.0) ],
|
||||
|
@ -590,7 +590,8 @@ impl LoadoutBuilder {
|
||||
.expect("coins should be stackable");
|
||||
*s = Some(coin_item);
|
||||
coins = 0;
|
||||
} else if let Some(item_id) = TradePricing::random_item(Good::Armor, armor)
|
||||
} else if let Some(item_id) =
|
||||
TradePricing::random_item(Good::Armor, armor, true)
|
||||
{
|
||||
*s = Some(Item::new_from_asset_expect(&item_id));
|
||||
}
|
||||
@ -605,7 +606,8 @@ impl LoadoutBuilder {
|
||||
.unwrap_or_default()
|
||||
/ 10.0;
|
||||
for i in bag1.slots_mut() {
|
||||
if let Some(item_id) = TradePricing::random_item(Good::Tools, weapon) {
|
||||
if let Some(item_id) = TradePricing::random_item(Good::Tools, weapon, true)
|
||||
{
|
||||
*i = Some(Item::new_from_asset_expect(&item_id));
|
||||
}
|
||||
}
|
||||
@ -615,7 +617,7 @@ impl LoadoutBuilder {
|
||||
let mut item = Item::new_from_asset_expect(&item_id);
|
||||
// NOTE: Conversion to and from f32 works fine because we make sure the
|
||||
// number we're converting is ≤ 100.
|
||||
let max = amount.min(100.min(item.max_amount()) as f32) as u32;
|
||||
let max = amount.min(16.min(item.max_amount()) as f32) as u32;
|
||||
let n = rng.gen_range(1..max.max(2));
|
||||
*amount -= if item.set_amount(n).is_ok() {
|
||||
n as f32
|
||||
@ -638,7 +640,7 @@ impl LoadoutBuilder {
|
||||
/ 10.0;
|
||||
for i in bag2.slots_mut() {
|
||||
if let Some(item_id) =
|
||||
TradePricing::random_item(Good::Ingredients, ingredients)
|
||||
TradePricing::random_item(Good::Ingredients, ingredients, true)
|
||||
{
|
||||
*i = item_with_amount(&item_id, &mut ingredients);
|
||||
}
|
||||
@ -658,7 +660,7 @@ impl LoadoutBuilder {
|
||||
.max(10000.0)
|
||||
/ 10.0;
|
||||
for i in bag3.slots_mut() {
|
||||
if let Some(item_id) = TradePricing::random_item(Good::Food, food) {
|
||||
if let Some(item_id) = TradePricing::random_item(Good::Food, food, true) {
|
||||
*i = item_with_amount(&item_id, &mut food);
|
||||
}
|
||||
}
|
||||
@ -672,7 +674,9 @@ impl LoadoutBuilder {
|
||||
.unwrap_or_default()
|
||||
/ 10.0;
|
||||
for i in bag4.slots_mut() {
|
||||
if let Some(item_id) = TradePricing::random_item(Good::Potions, potions) {
|
||||
if let Some(item_id) =
|
||||
TradePricing::random_item(Good::Potions, potions, true)
|
||||
{
|
||||
*i = item_with_amount(&item_id, &mut potions);
|
||||
}
|
||||
}
|
||||
|
@ -10,7 +10,7 @@ use lazy_static::lazy_static;
|
||||
use serde::Deserialize;
|
||||
use tracing::{info, warn};
|
||||
|
||||
type Entry = (String, f32);
|
||||
type Entry = (String, f32, bool);
|
||||
|
||||
type Entries = Vec<Entry>;
|
||||
const PRICING_DEBUG: bool = false;
|
||||
@ -72,7 +72,7 @@ impl From<Vec<(f32, LootSpec)>> for ProbabilityFile {
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct TradingPriceFile {
|
||||
pub loot_tables: Vec<(f32, String)>,
|
||||
pub loot_tables: Vec<(f32, bool, String)>,
|
||||
pub good_scaling: Vec<(Good, f32)>, // the amount of Good equivalent to the most common item
|
||||
}
|
||||
|
||||
@ -168,7 +168,7 @@ impl TradePricing {
|
||||
}
|
||||
|
||||
fn read() -> Self {
|
||||
fn add(entryvec: &mut Entries, itemname: &str, probability: f32) {
|
||||
fn add(entryvec: &mut Entries, itemname: &str, probability: f32, can_sell: bool) {
|
||||
let val = entryvec.iter_mut().find(|j| *j.0 == *itemname);
|
||||
if let Some(r) = val {
|
||||
if PRICING_DEBUG {
|
||||
@ -179,13 +179,13 @@ impl TradePricing {
|
||||
if PRICING_DEBUG {
|
||||
info!("New {} {}", itemname, probability);
|
||||
}
|
||||
entryvec.push((itemname.to_string(), probability));
|
||||
entryvec.push((itemname.to_string(), probability, can_sell));
|
||||
}
|
||||
}
|
||||
fn sort_and_normalize(entryvec: &mut [Entry], scale: f32) {
|
||||
if !entryvec.is_empty() {
|
||||
entryvec.sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap());
|
||||
if let Some((_, max_scale)) = entryvec.last() {
|
||||
if let Some((_, max_scale, _)) = entryvec.last() {
|
||||
// most common item has frequency max_scale. avoid NaN
|
||||
let rescale = scale / max_scale;
|
||||
for i in entryvec.iter_mut() {
|
||||
@ -210,9 +210,9 @@ impl TradePricing {
|
||||
if PRICING_DEBUG {
|
||||
info!(?i);
|
||||
}
|
||||
let loot = ProbabilityFile::load_expect(&i.1);
|
||||
let loot = ProbabilityFile::load_expect(&i.2);
|
||||
for j in loot.read().content.iter() {
|
||||
add(&mut result.get_list_by_path_mut(&j.1), &j.1, i.0 * j.0);
|
||||
add(&mut result.get_list_by_path_mut(&j.1), &j.1, i.0 * j.0, i.1);
|
||||
}
|
||||
}
|
||||
|
||||
@ -241,8 +241,8 @@ impl TradePricing {
|
||||
fn price_lookup(s: &TradePricing, name: &str) -> f32 {
|
||||
let vec = s.get_list_by_path(name);
|
||||
vec.iter()
|
||||
.find(|(n, _)| n == name)
|
||||
.map(|(_, freq)| 1.0 / freq)
|
||||
.find(|(n, _, _)| n == name)
|
||||
.map(|(_, freq, _)| 1.0 / freq)
|
||||
// even if we multiply by INVEST_FACTOR we need to remain above UNAVAILABLE_PRICE (add 1.0 to compensate rounding errors)
|
||||
.unwrap_or(TradePricing::UNAVAILABLE_PRICE/TradePricing::INVEST_FACTOR+1.0)
|
||||
}
|
||||
@ -276,6 +276,7 @@ impl TradePricing {
|
||||
&mut result.get_list_by_path_mut(&e.output),
|
||||
&e.output,
|
||||
(e.amount as f32) / actual_cost * TradePricing::CRAFTING_FACTOR,
|
||||
true,
|
||||
);
|
||||
false
|
||||
} else {
|
||||
@ -305,7 +306,7 @@ impl TradePricing {
|
||||
result
|
||||
}
|
||||
|
||||
fn random_item_impl(&self, good: Good, amount: f32) -> Option<String> {
|
||||
fn random_item_impl(&self, good: Good, amount: f32, selling: bool) -> Option<String> {
|
||||
if good == Good::Coin {
|
||||
Some(TradePricing::COIN_ITEM.into())
|
||||
} else {
|
||||
@ -321,14 +322,19 @@ impl TradePricing {
|
||||
.find(|i| i.1.1 * amount >= 1.0)
|
||||
.map(|i| i.0)
|
||||
.unwrap_or(upper - 1);
|
||||
let index = (rand::random::<f32>() * ((upper - lower) as f32)).floor() as usize + lower;
|
||||
//.gen_range(lower..upper);
|
||||
table.get(index).map(|i| i.0.clone())
|
||||
loop {
|
||||
let index =
|
||||
(rand::random::<f32>() * ((upper - lower) as f32)).floor() as usize + lower;
|
||||
//.gen_range(lower..upper);
|
||||
if table.get(index).map_or(false, |i| !selling || i.2) {
|
||||
break table.get(index).map(|i| i.0.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn random_item(good: Good, amount: f32) -> Option<String> {
|
||||
TRADE_PRICING.random_item_impl(good, amount)
|
||||
pub fn random_item(good: Good, amount: f32, selling: bool) -> Option<String> {
|
||||
TRADE_PRICING.random_item_impl(good, amount, selling)
|
||||
}
|
||||
|
||||
pub fn get_material(item: &str) -> (Good, f32) {
|
||||
@ -350,7 +356,7 @@ impl TradePricing {
|
||||
use crate::comp::item::{armor, tool, Item, ItemKind};
|
||||
|
||||
// we pass the item and the inverse of the price to the closure
|
||||
fn printvec<F>(x: &str, e: &[(String, f32)], f: F)
|
||||
fn printvec<F>(x: &str, e: &[(String, f32, bool)], f: F)
|
||||
where
|
||||
F: Fn(&Item, f32) -> String,
|
||||
{
|
||||
@ -437,7 +443,7 @@ mod tests {
|
||||
|
||||
TradePricing::instance().print_sorted();
|
||||
for _ in 0..5 {
|
||||
if let Some(item_id) = TradePricing::random_item(Good::Armor, 5.0) {
|
||||
if let Some(item_id) = TradePricing::random_item(Good::Armor, 5.0, false) {
|
||||
info!("Armor 5 {}", item_id);
|
||||
}
|
||||
}
|
||||
|
@ -1095,8 +1095,7 @@ mod tests {
|
||||
.iter()
|
||||
.map(|(good, a)| ResourcesSetup {
|
||||
good,
|
||||
amount: (*a as f32)
|
||||
* i.economy.natural_resources.average_yield_per_chunk[good],
|
||||
amount: *a * i.economy.natural_resources.average_yield_per_chunk[good],
|
||||
})
|
||||
.collect();
|
||||
let neighbors = i
|
||||
@ -1159,8 +1158,7 @@ mod tests {
|
||||
//let c = sim::SimChunk::new();
|
||||
//settlement.economy.add_chunk(ch, distance_squared)
|
||||
// bypass the API for now
|
||||
settlement.economy.natural_resources.chunks_per_resource[g.good] =
|
||||
g.amount as u32;
|
||||
settlement.economy.natural_resources.chunks_per_resource[g.good] = g.amount;
|
||||
settlement.economy.natural_resources.average_yield_per_chunk[g.good] = 1.0;
|
||||
}
|
||||
index.sites.insert(settlement);
|
||||
|
@ -29,7 +29,7 @@ pub struct Labor(u8, PhantomData<Profession>);
|
||||
#[derive(Debug)]
|
||||
pub struct AreaResources {
|
||||
pub resource_sum: MapVec<Good, f32>,
|
||||
pub resource_chunks: MapVec<Good, u32>,
|
||||
pub resource_chunks: MapVec<Good, f32>,
|
||||
pub chunks: u32,
|
||||
}
|
||||
|
||||
@ -49,7 +49,7 @@ pub struct NaturalResources {
|
||||
pub per_area: Vec<AreaResources>,
|
||||
|
||||
// computation simplifying cached values
|
||||
pub chunks_per_resource: MapVec<Good, u32>,
|
||||
pub chunks_per_resource: MapVec<Good, f32>,
|
||||
pub average_yield_per_chunk: MapVec<Good, f32>,
|
||||
}
|
||||
|
||||
@ -221,9 +221,9 @@ impl Economy {
|
||||
.iter()
|
||||
.map(|a| a.resource_chunks[g])
|
||||
.sum();
|
||||
if chunks != 0 {
|
||||
if chunks > 0.001 {
|
||||
self.natural_resources.chunks_per_resource[g] = chunks;
|
||||
self.natural_resources.average_yield_per_chunk[g] = amount / (chunks as f32);
|
||||
self.natural_resources.average_yield_per_chunk[g] = amount / chunks;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -273,14 +273,14 @@ impl Economy {
|
||||
|
||||
pub fn replenish(&mut self, _time: f32) {
|
||||
for (good, &ch) in self.natural_resources.chunks_per_resource.iter() {
|
||||
let per_year = self.natural_resources.average_yield_per_chunk[good] * (ch as f32);
|
||||
let per_year = self.natural_resources.average_yield_per_chunk[good] * ch;
|
||||
self.stocks[good] = self.stocks[good].max(per_year);
|
||||
}
|
||||
// info!("resources {:?}", self.stocks);
|
||||
}
|
||||
|
||||
pub fn add_chunk(&mut self, ch: &SimChunk, distance_squared: i64) {
|
||||
let biome = ch.get_biome();
|
||||
// let biome = ch.get_biome();
|
||||
// we don't scale by pi, although that would be correct
|
||||
let distance_bin = (distance_squared >> 16).min(64) as usize;
|
||||
if self.natural_resources.per_area.len() <= distance_bin {
|
||||
@ -289,9 +289,30 @@ impl Economy {
|
||||
.resize_with(distance_bin + 1, Default::default);
|
||||
}
|
||||
self.natural_resources.per_area[distance_bin].chunks += 1;
|
||||
self.natural_resources.per_area[distance_bin].resource_sum[Terrain(biome)] += 1.0;
|
||||
self.natural_resources.per_area[distance_bin].resource_chunks[Terrain(biome)] += 1;
|
||||
// TODO: Scale resources by rockiness or tree_density?
|
||||
// self.natural_resources.per_area[distance_bin].resource_sum[Terrain(biome)] +=
|
||||
// 1.0; self.natural_resources.per_area[distance_bin].
|
||||
// resource_chunks[Terrain(biome)] += 1.0; TODO: Scale resources by
|
||||
// rockiness or tree_density?
|
||||
|
||||
let mut add_biome = |biome, amount| {
|
||||
self.natural_resources.per_area[distance_bin].resource_sum[Terrain(biome)] += amount;
|
||||
self.natural_resources.per_area[distance_bin].resource_chunks[Terrain(biome)] += amount;
|
||||
};
|
||||
if ch.river.is_ocean() {
|
||||
add_biome(BiomeKind::Ocean, 1.0);
|
||||
} else if ch.river.is_lake() {
|
||||
add_biome(BiomeKind::Lake, 1.0);
|
||||
} else {
|
||||
add_biome(BiomeKind::Forest, 0.5 + ch.tree_density);
|
||||
add_biome(BiomeKind::Grassland, 0.5 + ch.humidity);
|
||||
add_biome(BiomeKind::Jungle, 0.5 + ch.humidity * ch.temp.max(0.0));
|
||||
add_biome(BiomeKind::Mountain, 0.5 + (ch.alt / 4000.0).max(0.0));
|
||||
add_biome(
|
||||
BiomeKind::Desert,
|
||||
0.5 + (1.0 - ch.humidity) * ch.temp.max(0.0),
|
||||
);
|
||||
add_biome(BiomeKind::Snowland, 0.5 + (-ch.temp).max(0.0));
|
||||
}
|
||||
}
|
||||
|
||||
pub fn add_neighbor(&mut self, id: Id<Site>, distance: usize) {
|
||||
@ -305,12 +326,26 @@ impl Economy {
|
||||
}
|
||||
|
||||
pub fn get_site_prices(&self) -> SitePrices {
|
||||
SitePrices {
|
||||
values: self
|
||||
.values
|
||||
let normalize = |xs: MapVec<Good, Option<f32>>| {
|
||||
let sum = xs
|
||||
.iter()
|
||||
.map(|(g, v)| (g, v.unwrap_or(Economy::MINIMUM_PRICE)))
|
||||
.collect(),
|
||||
.map(|(_, x)| (*x).unwrap_or(0.0))
|
||||
.sum::<f32>()
|
||||
.max(0.001);
|
||||
xs.map(|_, x| Some(x? / sum))
|
||||
};
|
||||
|
||||
SitePrices {
|
||||
values: {
|
||||
let labor_values = normalize(self.labor_values.clone());
|
||||
// Use labor values as prices. Not correct (doesn't care about exchange value)
|
||||
let prices = normalize(self.values.clone())
|
||||
.map(|good, value| Some((labor_values[good]? + value?) * 0.5));
|
||||
prices
|
||||
.iter()
|
||||
.map(|(g, v)| (g, v.unwrap_or(Economy::MINIMUM_PRICE)))
|
||||
.collect()
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user