diff --git a/CHANGELOG.md b/CHANGELOG.md index c379c70fb3..e6541c0757 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -141,6 +141,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Mindflayer AI now correctly summons husks at certain HP thresholds. - Far away NPCs respond to being damaged by a projectile - Fixed terrain clipping with glider +- Fixed an issue where prices weren't properly making their way from econsim to the actual trade values. ## [0.9.0] - 2021-03-20 diff --git a/Cargo.lock b/Cargo.lock index 698b7af1b1..3ccaed6fb5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5900,6 +5900,7 @@ dependencies = [ "bincode", "bitvec", "criterion", + "csv", "deflate 0.9.1", "enum-iterator", "flate2", @@ -5919,8 +5920,10 @@ dependencies = [ "rand_chacha 0.3.0", "rayon", "ron", + "rusqlite", "serde", "structopt", + "strum", "svg_fmt", "tracing", "tracing-subscriber", diff --git a/assets/common/item_price_calculation.ron b/assets/common/item_price_calculation.ron index 0bc268b1e7..c2825e5e7d 100644 --- a/assets/common/item_price_calculation.ron +++ b/assets/common/item_price_calculation.ron @@ -41,6 +41,6 @@ good_scaling: [ (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 + (Tools, 0.10), // common.items.weapons.staff.starter_staff + (Ingredients, 0.15), // common.items.crafting_ing.leather_scraps ]) diff --git a/common/src/comp/inventory/loadout_builder.rs b/common/src/comp/inventory/loadout_builder.rs index 8d7cd0bf79..8769aa53af 100644 --- a/common/src/comp/inventory/loadout_builder.rs +++ b/common/src/comp/inventory/loadout_builder.rs @@ -551,10 +551,12 @@ 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, true) - { - *s = Some(Item::new_from_asset_expect(&item_id)); + } else if armor > 0.0 { + if let Some(item_id) = + TradePricing::random_item(Good::Armor, armor, true) + { + *s = Some(Item::new_from_asset_expect(&item_id)); + } } } let mut bag1 = Item::new_from_asset_expect( @@ -566,10 +568,13 @@ impl LoadoutBuilder { .copied() .unwrap_or_default() / 10.0; - for i in bag1.slots_mut() { - if let Some(item_id) = TradePricing::random_item(Good::Tools, weapon, true) - { - *i = Some(Item::new_from_asset_expect(&item_id)); + if weapon > 0.0 { + for i in bag1.slots_mut() { + if let Some(item_id) = + TradePricing::random_item(Good::Tools, weapon, true) + { + *i = Some(Item::new_from_asset_expect(&item_id)); + } } } let mut rng = rand::thread_rng(); diff --git a/common/src/terrain/biome.rs b/common/src/terrain/biome.rs index 9c0ce56e87..3bd46fd05c 100644 --- a/common/src/terrain/biome.rs +++ b/common/src/terrain/biome.rs @@ -1,6 +1,7 @@ use serde::{Deserialize, Serialize}; +use strum_macros::EnumIter; -#[derive(Debug, Copy, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)] +#[derive(Debug, Copy, Clone, Serialize, Deserialize, PartialEq, Eq, Hash, EnumIter)] pub enum BiomeKind { Void, Lake, @@ -13,3 +14,7 @@ pub enum BiomeKind { Jungle, Forest, } + +impl Default for BiomeKind { + fn default() -> BiomeKind { BiomeKind::Void } +} diff --git a/common/src/trade.rs b/common/src/trade.rs index 33aa6dae32..d666fce609 100644 --- a/common/src/trade.rs +++ b/common/src/trade.rs @@ -5,6 +5,7 @@ use crate::{ }; use hashbrown::HashMap; use serde::{Deserialize, Serialize}; +use strum_macros::EnumIter; use tracing::{trace, warn}; #[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)] @@ -296,7 +297,7 @@ impl Default for Trades { // we need this declaration in common for Merchant loadout creation, it is not // directly related to trade between entities, but between sites (more abstract) // economical information -#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, Serialize, Deserialize)] +#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, Serialize, Deserialize, EnumIter)] pub enum Good { Territory(BiomeKind), Flour, diff --git a/world/Cargo.toml b/world/Cargo.toml index c96c114004..e0f3bc3f84 100644 --- a/world/Cargo.toml +++ b/world/Cargo.toml @@ -7,7 +7,7 @@ edition = "2018" [features] tracy = ["common/tracy", "common-net/tracy"] simd = ["vek/platform_intrinsics"] -bin_compression = ["lz-fear", "deflate", "flate2", "common-frontend", "image/jpeg", "num-traits"] +bin_compression = ["lz-fear", "deflate", "flate2", "image/jpeg", "num-traits"] default = ["simd"] @@ -43,15 +43,18 @@ lz-fear = { version = "0.1.1", optional = true } deflate = { version = "0.9.1", optional = true } flate2 = { version = "1.0.20", optional = true } num-traits = { version = "0.2", optional = true } -common-frontend = { package = "veloren-common-frontend", path = "../common/frontend", optional = true } [dev-dependencies] +common-frontend = { package = "veloren-common-frontend", path = "../common/frontend" } criterion = "0.3" +csv = "1.1.3" tracing-subscriber = { version = "0.2.15", default-features = false, features = ["fmt", "chrono", "ansi", "smallvec", "env-filter"] } minifb = "0.19.1" +rusqlite = { version = "0.24.2", features = ["array", "vtab", "bundled", "trace"] } svg_fmt = "0.4" structopt = "0.3" +strum = "0.20" [[bench]] harness = false diff --git a/world/examples/pricing_csv.rs b/world/examples/pricing_csv.rs new file mode 100644 index 0000000000..8d9c0975f5 --- /dev/null +++ b/world/examples/pricing_csv.rs @@ -0,0 +1,177 @@ +use common::{ + terrain::BiomeKind, + trade::{Good, SitePrices}, +}; +use rayon::ThreadPoolBuilder; +use rusqlite::{Connection, ToSql}; +use std::error::Error; +use strum::IntoEnumIterator; +use vek::Vec2; +use veloren_world::{ + index::Index, + sim::{FileOpts, WorldOpts, DEFAULT_WORLD_MAP}, + World, +}; + +fn good_pricing_csv(world: &World, index: &Index) -> Result<(), Box> { + let mut csv = csv::Writer::from_path("good_pricing.csv")?; + csv.write_record(&[ + "Site", + "XCoord", + "YCoord", + "Flour", + "Meat", + "Transportation", + "Food", + "Wood", + "Stone", + "Tools", + "Armor", + "Ingredients", + "Potions", + "Coin", + "RoadSecurity", + ])?; + + for civsite in world.civs().sites() { + if let Some(site_id) = civsite.site_tmp { + let site = index.sites.get(site_id); + if site.do_economic_simulation() { + let prices = site.economy.get_site_prices(); + //println!("{:?}: {:?} {:?}", site.name(), civsite.center, prices); + csv.write_record(&[ + site.name(), + &format!("{}", civsite.center.x), + &format!("{}", civsite.center.y), + &format!("{}", prices.values.get(&Good::Flour).unwrap_or(&0.0)), + &format!("{}", prices.values.get(&Good::Meat).unwrap_or(&0.0)), + &format!( + "{}", + prices.values.get(&Good::Transportation).unwrap_or(&0.0) + ), + &format!("{}", prices.values.get(&Good::Food).unwrap_or(&0.0)), + &format!("{}", prices.values.get(&Good::Wood).unwrap_or(&0.0)), + &format!("{}", prices.values.get(&Good::Stone).unwrap_or(&0.0)), + &format!("{}", prices.values.get(&Good::Tools).unwrap_or(&0.0)), + &format!("{}", prices.values.get(&Good::Armor).unwrap_or(&0.0)), + &format!("{}", prices.values.get(&Good::Ingredients).unwrap_or(&0.0)), + &format!("{}", prices.values.get(&Good::Potions).unwrap_or(&0.0)), + &format!("{}", prices.values.get(&Good::Coin).unwrap_or(&0.0)), + &format!("{}", prices.values.get(&Good::RoadSecurity).unwrap_or(&0.0)), + ])?; + } + } + } + + Ok(()) +} + +fn economy_sqlite(world: &World, index: &Index) -> Result<(), Box> { + let conn = Connection::open("economy.sqlite")?; + #[rustfmt::skip] + conn.execute_batch(" + CREATE TABLE IF NOT EXISTS site ( + xcoord INTEGER NOT NULL, + ycoord INTEGER NUT NULL, + name TEXT NOT NULL + ); + CREATE UNIQUE INDEX IF NOT EXISTS site_position ON site(xcoord, ycoord); + CREATE TABLE IF NOT EXISTS site_price ( + xcoord INTEGER NOT NULL, + ycoord INTEGER NOT NULL, + good TEXT NOT NULL, + price REAL NOT NULL + ); + CREATE UNIQUE INDEX IF NOT EXISTS site_good on site_price(xcoord, ycoord, good); + ")?; + let mut all_goods = Vec::new(); + for good in Good::iter() { + match good { + Good::Territory(_) => { + for biome in BiomeKind::iter() { + all_goods.push(Good::Territory(biome)); + } + }, + Good::Terrain(_) => { + for biome in BiomeKind::iter() { + all_goods.push(Good::Terrain(biome)); + } + }, + _ => { + all_goods.push(good); + }, + } + } + + let mut good_columns = String::new(); + let mut good_exprs = String::new(); + for good in all_goods.iter() { + good_columns += &format!(", '{:?}'", good); + good_exprs += &format!( + ", MAX(CASE WHEN site_price.good = '{:?}' THEN site_price.price END)", + good + ); + } + + #[rustfmt::skip] + let create_view = format!(" + CREATE VIEW IF NOT EXISTS site_price_tr (xcoord, ycoord {}) + AS SELECT xcoord, ycoord {} + FROM site NATURAL JOIN site_price + GROUP BY xcoord, ycoord + ", good_columns, good_exprs); + conn.execute_batch(&create_view)?; + let mut insert_price_stmt = conn + .prepare("REPLACE INTO site_price (xcoord, ycoord, good, price) VALUES (?1, ?2, ?3, ?4)")?; + let mut insert_price = move |center: Vec2, good: Good, prices: &SitePrices| { + let price = prices.values.get(&good).unwrap_or(&0.0); + insert_price_stmt.execute(&[ + ¢er.x as &dyn ToSql, + ¢er.y, + &format!("{:?}", good), + &(*price as f64), + ]) + }; + for civsite in world.civs().sites() { + if let Some(site_id) = civsite.site_tmp { + let site = index.sites.get(site_id); + if site.do_economic_simulation() { + let prices = site.economy.get_site_prices(); + conn.execute( + "REPLACE INTO site (xcoord, ycoord, name) VALUES (?1, ?2, ?3)", + &[ + &civsite.center.x as &dyn ToSql, + &civsite.center.y, + &site.name(), + ], + )?; + for good in all_goods.iter() { + insert_price(civsite.center, *good, &prices)?; + } + } + } + } + Ok(()) +} + +fn main() { + common_frontend::init_stdout(None); + println!("Loading world"); + let pool = ThreadPoolBuilder::new().build().unwrap(); + let (world, index) = World::generate( + 59686, + WorldOpts { + seed_elements: true, + world_file: FileOpts::LoadAsset(DEFAULT_WORLD_MAP.into()), + }, + &pool, + ); + println!("Loaded world"); + + if let Err(e) = good_pricing_csv(&world, &index) { + println!("Error generating goodpricing csv: {:?}", e); + } + if let Err(e) = economy_sqlite(&world, &index) { + println!("Error generating economy sqlite db: {:?}", e); + } +} diff --git a/world/src/site/economy.rs b/world/src/site/economy.rs index a40d600525..e842b543b2 100644 --- a/world/src/site/economy.rs +++ b/world/src/site/economy.rs @@ -339,12 +339,12 @@ impl Economy { 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() + let prices = normalize(self.values.clone()).map(|good, value| { + (labor_values[good].unwrap_or(Economy::MINIMUM_PRICE) + + value.unwrap_or(Economy::MINIMUM_PRICE)) + * 0.5 + }); + prices.iter().map(|(g, v)| (g, *v)).collect() }, } }