diff --git a/README.md b/README.md index 6b10e4b..34041df 100644 --- a/README.md +++ b/README.md @@ -101,11 +101,17 @@ announcement = "I love cheese! I am at {location}." # values are the price in coins. You may type in-game "/give_item common.items." and press Tab to # explore the item definition IDs. Then just leave off the "common.items." part in this file. -[buy_prices] +[buy_prices.simple] "food.cheese" = 50 -[sell_prices] +[sell_prices.simple] "consumable.potion_minor" = 150 + +[sell_prices.modular] +material = "mineral.ingot.orichalcum" +primary = "sword.greatsword" +seconday = "sword.long" +price = 45_000 ``` ### Running diff --git a/config/config.toml b/config/config.toml index a6e7902..7d0effb 100644 --- a/config/config.toml +++ b/config/config.toml @@ -1,63 +1,20 @@ position = [17720.0, 14951.0, 237.0] orientation = 0 -[buy_prices] +[buy_prices.simple] "food.cheese" = 50 -[sell_prices] -# Armor -"armor.misc.neck.carcanet_of_wrath" = 20_000 +[[buy_prices.modular]] +material = "Iron" +primary = "sword.greatsword" +secondary = "sword.long" +price = 1000 -## Boreal Armor -"armor.boreal.back" = 250_000 -"armor.boreal.belt" = 250_000 -"armor.boreal.chest" = 250_000 -"armor.boreal.foot" = 250_000 -"armor.boreal.hand" = 250_000 -"armor.boreal.pants" = 250_000 -"armor.boreal.shoulder" = 250_000 -"armor.misc.head.boreal_warhelm" = 450_000 - -# Hats -"armor.misc.head.cat_capuche" = 750_000 -"armor.misc.head.hare_hat" = 150_000 -"armor.misc.head.winged_coronet" = 40_000 -"calendar.christmas.armor.misc.head.woolly_wintercap" = 250_000 - -# Crafting -"crafting_ing.alkahest" = 6_000 -"crafting_ing.brinestone" = 2_000 -"crafting_ing.coral_branch" = 1_000 -"crafting_ing.hide.dragon_scale" = 5_000 -"crafting_ing.dwarven_battery" = 40_000 -"log.eldwood" = 3_000 -"mineral.ingot.orichalcum" = 8_000 -"mineral.ore.ancient_gold" = 10_000 - -# Potions +[sell_prices.simple] "consumable.potion_minor" = 150 -# Gliders -"glider.skullgrin" = 20_000 - -# Recipes -"recipes.armor.brinestone" = 8_000 -"recipes.equipment.advanced" = 8_000 -"recipes.unique.mindflayer_spellbag" = 10_000 -"recipes.unique.abyssal_gorget" = 6_000 - -# Instruments -"tool.instruments.icy_talharpa" = 500_000 -"tool.instruments.steeltonguedrum" = 300_000 - -# Legendary Weapons -"weapons.axe.parashu" = 100_000 -"weapons.sword.caladbolg" = 150_000 -"weapons.staff.laevateinn" = 60_000 -"weapons.hammer.mjolnir" = 150_000 -"weapons.sceptre.caduceus" = 150_000 - -# Lanterns -"boss_drops.lantern" = 40_000 # Magic Lantern -"lantern.blue_0" = 20_000 -"lantern.geode_purp" = 20_000 +[[sell_prices.modular]] +material = "Iron" +primary = "sword.greatsword" +secondary = "sword.long" +price = 1000 diff --git a/src/bot.rs b/src/bot.rs index d837909..86959eb 100644 --- a/src/bot.rs +++ b/src/bot.rs @@ -16,7 +16,10 @@ use veloren_common::{ clock::Clock, comp::{ invite::InviteKind, - item::{ItemDefinitionId, ItemDefinitionIdOwned, ItemDesc, ItemI18n, MaterialStatManifest}, + item::{ + modular, ItemBase, ItemDef, ItemDefinitionId, ItemDefinitionIdOwned, ItemDesc, + ItemI18n, MaterialStatManifest, + }, slot::InvSlotId, tool::AbilityMap, ChatType, ControllerInputs, Item, Ori, Pos, @@ -30,7 +33,10 @@ use veloren_common::{ }; use veloren_common_net::sync::WorldSyncExt; +use crate::PriceList; + const COINS: &str = "common.items.utility.coins"; +const COINS_ID: ItemDefinitionId = ItemDefinitionId::Simple(Cow::Borrowed(COINS)); /// An active connection to the Veloren server that will attempt to run every time the `tick` /// function is called. @@ -50,8 +56,8 @@ pub struct Bot { item_i18n: ItemI18n, localization: LocalizationHandle, - buy_prices: HashMap, - sell_prices: HashMap, + buy_prices: PriceList, + sell_prices: PriceList, trade_mode: TradeMode, previous_offer: Option<(HashMap, HashMap)>, @@ -72,8 +78,8 @@ impl Bot { password: &str, character: &str, admins: Vec, - buy_prices: HashMap, - sell_prices: HashMap, + buy_prices: PriceList, + sell_prices: PriceList, position: Option<[f32; 3]>, orientation: Option, announcement: Option, @@ -547,9 +553,9 @@ impl Bot { inventories.get(them).ok_or("Failed to find inventory")?, ); - let coins = ItemDefinitionIdOwned::Simple(COINS.to_string()); - let get_my_coins = my_inventory.get_slot_of_item_by_def_id(&coins); - let get_their_coins = their_inventory.get_slot_of_item_by_def_id(&coins); + let coins_owned = COINS_ID.to_owned(); + let get_my_coins = my_inventory.get_slot_of_item_by_def_id(&coins_owned); + let get_their_coins = their_inventory.get_slot_of_item_by_def_id(&coins_owned); let (mut my_offered_coin_amount, mut their_offered_coin_amount) = (0, 0); let my_offered_items_value = @@ -557,18 +563,20 @@ impl Bot { .into_iter() .fold(0, |acc: i32, (slot_id, quantity)| { if let Some(item) = my_inventory.get(*slot_id) { - let item_id = item.persistence_item_id(); + let item_id = item.item_definition_id(); - let item_value = if item_id == COINS { + let item_value = if item_id == COINS_ID { my_offered_coin_amount = *quantity as i32; 1 } else { self.sell_prices + .simple .get(&item_id) .map(|int| *int as i32) .unwrap_or_else(|| { self.buy_prices + .simple .get(&item_id) .map(|int| 0 - *int as i32) .unwrap_or(i32::MIN) @@ -585,18 +593,20 @@ impl Bot { .into_iter() .fold(0, |acc: i32, (slot_id, quantity)| { if let Some(item) = their_inventory.get(*slot_id) { - let item_id = item.persistence_item_id(); + let item_id = item.item_definition_id(); - let item_value = if item_id == COINS { + let item_value = if item_id == COINS_ID { their_offered_coin_amount = *quantity as i32; 1 } else { self.buy_prices + .simple .get(&item_id) .map(|int| *int as i32) .unwrap_or_else(|| { self.sell_prices + .simple .get(&item_id) .map(|int| 0 - *int as i32) .unwrap_or(0) @@ -613,13 +623,13 @@ impl Bot { for (slot_id, amount) in my_offer { let item = my_inventory.get(*slot_id).ok_or("Failed to get item")?; - let item_id = item.persistence_item_id(); + let item_id = item.item_definition_id(); - if item_id == COINS { + if item_id == COINS_ID { continue; } - if !self.sell_prices.contains_key(&item_id) { + if !self.sell_prices.simple.contains_key(&item_id) { my_item_to_remove = Some((slot_id, amount)); } } @@ -628,13 +638,13 @@ impl Bot { for (slot_id, amount) in their_offer { let item = their_inventory.get(*slot_id).ok_or("Failed to get item")?; - let item_id = item.persistence_item_id(); + let item_id = item.item_definition_id(); - if item_id == COINS { + if item_id == COINS_ID { continue; } - if !self.buy_prices.contains_key(&item_id) { + if !self.buy_prices.simple.contains_key(&item_id) { their_item_to_remove = Some((slot_id, amount)); } } @@ -741,8 +751,8 @@ impl Bot { .to_string(); let mut found = false; - for (item_id, price) in &self.buy_prices { - let item_name = self.get_item_name(item_id); + for (item_id, price) in &self.buy_prices.simple { + let item_name = self.get_item_name(item_id.as_ref()); if item_name.to_lowercase().contains(&search_term) { log::info!("Sending price info on {item_name} to {player_name}"); @@ -756,16 +766,60 @@ impl Bot { ); found = true; - } else if item_id.contains(&search_term) { - let short_id = item_id.splitn(3, '.').last().unwrap_or_default(); - log::info!("Sending price info on {short_id} to {player_name}"); + continue; + } + + if let Some(item_id_string) = item_id.as_ref().itemdef_id() { + if item_id_string.to_lowercase().contains(&search_term) { + log::info!("Sending price info on {item_id_string} to {player_name}"); + + self.client.send_command( + "tell".to_string(), + vec![ + player_name.clone(), + format!("Buying {item_id_string} for {price} coins."), + ], + ); + + found = true; + } + } + } + + for modular_item_listing in &self.buy_prices.modular { + let material = ItemDefinitionId::Simple(Cow::Borrowed( + modular_item_listing + .material + .asset_identifier() + .ok_or(format!( + "{:?} is not a valid material for modular crafted items", + modular_item_listing.material + ))?, + )); + let primary = modular_item_listing.primary.as_ref(); + let secondary = ItemDefinitionId::Compound { + // This unwrap is safe because the ItemDefinitionId is always Simple. + simple_base: primary.itemdef_id().unwrap(), + components: vec![material], + }; + let item_id = ItemDefinitionId::Modular { + pseudo_base: "veloren.core.pseudo_items.modular.tool", + components: vec![secondary], + }; + let item_name = self.get_item_name(item_id); + + if item_name.to_lowercase().contains(&search_term) { + log::info!("Sending price info on {item_name} to {player_name}"); self.client.send_command( "tell".to_string(), vec![ player_name.clone(), - format!("Buying {short_id} for {price} coins."), + format!( + "Buying {item_name} for {} coins.", + modular_item_listing.price + ), ], ); @@ -773,8 +827,8 @@ impl Bot { } } - for (item_id, price) in &self.sell_prices { - let item_name = self.get_item_name(item_id); + for (item_id, price) in &self.sell_prices.simple { + let item_name = self.get_item_name(item_id.as_ref()); if item_name.to_lowercase().contains(&search_term) { log::info!("Sending price info on {item_name} to {player_name}"); @@ -788,16 +842,60 @@ impl Bot { ); found = true; - } else if item_id.contains(&search_term) { - let short_id = item_id.splitn(3, '.').last().unwrap_or_default(); - log::info!("Sending price info on {short_id} to {player_name}"); + continue; + } + + if let Some(item_id_string) = item_id.as_ref().itemdef_id() { + if item_id_string.to_lowercase().contains(&search_term) { + log::info!("Sending price info on {item_id_string} to {player_name}"); + + self.client.send_command( + "tell".to_string(), + vec![ + player_name.clone(), + format!("Selling {item_id_string} for {price} coins."), + ], + ); + + found = true; + } + } + } + + for modular_item_listing in &self.sell_prices.modular { + let material = ItemDefinitionId::Simple(Cow::Borrowed( + modular_item_listing + .material + .asset_identifier() + .ok_or(format!( + "{:?} is not a valid material for modular crafted items", + modular_item_listing.material + ))?, + )); + let primary = modular_item_listing.primary.as_ref(); + let secondary = ItemDefinitionId::Compound { + // This unwrap is safe because the ItemDefinitionId is always Simple. + simple_base: primary.itemdef_id().unwrap(), + components: vec![material], + }; + let item_id = ItemDefinitionId::Modular { + pseudo_base: "veloren.core.pseudo_items.modular.tool", + components: vec![secondary], + }; + let item_name = self.get_item_name(item_id); + + if item_name.to_lowercase().contains(&search_term) { + log::info!("Sending price info on {item_name} to {player_name}"); self.client.send_command( "tell".to_string(), vec![ player_name.clone(), - format!("Selling {short_id} for {price} coins."), + format!( + "Selling {item_name} for {} coins.", + modular_item_listing.price + ), ], ); @@ -866,13 +964,10 @@ impl Bot { } /// Gets the name of an item from its id. - fn get_item_name(&self, item_id: &str) -> String { - let item = Item::new_from_item_definition_id( - ItemDefinitionId::Simple(Cow::Borrowed(item_id)), - &self.ability_map, - &self.material_manifest, - ) - .unwrap(); + fn get_item_name(&self, item_id: ItemDefinitionId) -> String { + let item = + Item::new_from_item_definition_id(item_id, &self.ability_map, &self.material_manifest) + .unwrap(); let (item_name_i18n_id, _) = item.i18n(&self.item_i18n); self.localization.read().get_content(&item_name_i18n_id) diff --git a/src/main.rs b/src/main.rs index c691c12..0e14bbc 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,24 +2,165 @@ mod bot; -use std::{env::var, fs::read_to_string}; +use std::{borrow::Cow, env::var, fs::read_to_string, str::FromStr}; use bot::Bot; use hashbrown::HashMap; -use serde::{Deserialize, Serialize}; +use serde::{de::Visitor, Deserialize}; +use veloren_common::comp::item::{ItemDefinitionId, ItemDefinitionIdOwned, Material}; -#[derive(Serialize, Deserialize)] +pub struct PriceList { + pub simple: HashMap, + pub modular: Vec, +} + +impl<'de> Deserialize<'de> for PriceList { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + deserializer.deserialize_map(PriceListVisitor) + } +} + +pub struct PriceListVisitor; + +impl<'de> Visitor<'de> for PriceListVisitor { + type Value = PriceList; + + fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { + formatter.write_str("a map with simple and/or modular keys") + } + + fn visit_map(self, mut map: A) -> Result + where + A: serde::de::MapAccess<'de>, + { + let mut simple = None; + let mut modular = None; + + while let Some(key) = map.next_key::()? { + match key.as_str() { + "simple" => { + let simple_prices_with_item_string = + map.next_value::>()?; + let simple_prices_with_item_id = simple_prices_with_item_string + .into_iter() + .map(|(mut key, value)| { + key.insert_str(0, "common.items."); + + (ItemDefinitionIdOwned::Simple(key), value) + }) + .collect(); + + simple = Some(simple_prices_with_item_id); + } + "modular" => { + modular = Some(map.next_value()?); + } + _ => { + return Err(serde::de::Error::unknown_field( + &key, + &["simple", "modular"], + )); + } + } + } + + Ok(PriceList { + simple: simple.ok_or_else(|| serde::de::Error::missing_field("simple"))?, + modular: modular.ok_or_else(|| serde::de::Error::missing_field("modular"))?, + }) + } +} + +#[derive(Debug, PartialEq, Eq, Hash)] +pub struct ModularItemPrice { + pub material: Material, + pub primary: ItemDefinitionIdOwned, + pub secondary: ItemDefinitionIdOwned, + pub price: u32, +} + +impl<'de> Deserialize<'de> for ModularItemPrice { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + deserializer.deserialize_map(ModularPriceVisitor) + } +} + +struct ModularPriceVisitor; + +impl<'de> Visitor<'de> for ModularPriceVisitor { + type Value = ModularItemPrice; + + fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { + formatter.write_str("a map with material, primary, secondary and price keys") + } + + fn visit_map(self, mut map: A) -> Result + where + A: serde::de::MapAccess<'de>, + { + let mut material = None; + let mut primary = None; + let mut secondary = None; + let mut price = None; + + while let Some(key) = map.next_key::()? { + match key.as_str() { + "material" => { + material = Some(map.next_value()?); + } + "primary" => { + let mut primary_string = map.next_value::()?; + + primary_string.insert_str(0, "common.items.modular.weapon.primary."); + + primary = Some(ItemDefinitionIdOwned::Simple(primary_string)); + } + "secondary" => { + let mut secondary_string = map.next_value::()?; + + secondary_string.insert_str(0, "common.items.modular.weapon.secondary."); + + secondary = Some(ItemDefinitionIdOwned::Simple(secondary_string)); + } + "price" => { + price = Some(map.next_value()?); + } + _ => { + return Err(serde::de::Error::unknown_field( + &key, + &["material", "primary", "secondary", "price"], + )); + } + } + } + + Ok(ModularItemPrice { + material: material.ok_or_else(|| serde::de::Error::missing_field("material"))?, + primary: primary.ok_or_else(|| serde::de::Error::missing_field("primary"))?, + secondary: secondary.ok_or_else(|| serde::de::Error::missing_field("secondary"))?, + price: price.ok_or_else(|| serde::de::Error::missing_field("price"))?, + }) + } +} + +#[derive(Deserialize)] struct Config { pub game_server: Option, pub auth_server: Option, pub position: Option<[f32; 3]>, pub orientation: Option, pub announcement: Option, - pub buy_prices: HashMap, - pub sell_prices: HashMap, + pub buy_prices: PriceList, + pub sell_prices: PriceList, } -#[derive(Serialize, Deserialize)] +#[derive(Deserialize)] pub struct Secrets { pub username: String, pub password: String, @@ -50,24 +191,6 @@ fn main() { let auth_server = config .auth_server .unwrap_or("https://auth.veloren.net".to_string()); - let buy_prices_with_full_id = config - .buy_prices - .into_iter() - .map(|(mut item_id, price)| { - item_id.insert_str(0, "common.items."); - - (item_id, price) - }) - .collect(); - let sell_prices_with_full_id = config - .sell_prices - .into_iter() - .map(|(mut item_id, price)| { - item_id.insert_str(0, "common.items."); - - (item_id, price) - }) - .collect(); let mut bot = Bot::new( game_server, &auth_server, @@ -75,8 +198,8 @@ fn main() { &secrets.password, &secrets.character, secrets.admins, - buy_prices_with_full_id, - sell_prices_with_full_id, + config.buy_prices, + config.sell_prices, config.position, config.orientation, config.announcement,