From 3684cf0454c44509aac5db591eff5628b5f1f3d8 Mon Sep 17 00:00:00 2001 From: Avi Weinstock Date: Fri, 28 May 2021 14:42:29 -0400 Subject: [PATCH] Fix economy data not properly being used to price trades, resulting in default prices being applied at most towns. Also add an exporter from econsim results to sqlite to aid in debugging the economy (which revealed this bug). --- CHANGELOG.md | 1 + Cargo.lock | 3 + assets/common/item_price_calculation.ron | 4 +- common/src/comp/inventory/loadout_builder.rs | 21 ++- common/src/terrain/biome.rs | 7 +- common/src/trade.rs | 3 +- world/Cargo.toml | 7 +- world/examples/pricing_csv.rs | 177 +++++++++++++++++++ world/src/site/economy.rs | 12 +- 9 files changed, 215 insertions(+), 20 deletions(-) create mode 100644 world/examples/pricing_csv.rs 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() }, } }