diff --git a/Cargo.lock b/Cargo.lock index 4d8e959..c91d316 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -60,6 +60,55 @@ dependencies = [ "libc", ] +[[package]] +name = "anstream" +version = "0.6.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "418c75fa768af9c03be99d17643f93f79bbba589895012a80e3452a19ddda15b" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "038dfcf04a5feb68e9c60b21c9625a54c2c0616e79b72b0fd87075a056ae1d1b" + +[[package]] +name = "anstyle-parse" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c03a11a9034d92058ceb6ee011ce58af4a9bf61491aa7e1e59ecd24bd40d22d4" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad186efb764318d35165f1758e7dcef3b10628e26d41a44bc5550652e6804391" +dependencies = [ + "windows-sys 0.52.0", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61a38449feb7068f52bb06c12759005cf459ee52bb4adc1d5a7c4322d716fb19" +dependencies = [ + "anstyle", + "windows-sys 0.52.0", +] + [[package]] name = "approx" version = "0.5.1" @@ -357,6 +406,12 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" +[[package]] +name = "colorchoice" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b6a852b24ab71dffc585bcb46eaf7959d175cb865a7152e35b348d1b2960422" + [[package]] name = "combine" version = "4.6.7" @@ -656,6 +711,29 @@ dependencies = [ "syn 2.0.66", ] +[[package]] +name = "env_filter" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a009aa4810eb158359dda09d0c87378e4bbb89b5a801f016885a4707ba24f7ea" +dependencies = [ + "log", + "regex", +] + +[[package]] +name = "env_logger" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38b35839ba51819680ba087cd351788c9a3c476841207e0b8cee0b04722343b9" +dependencies = [ + "anstream", + "anstyle", + "env_filter", + "humantime", + "log", +] + [[package]] name = "equivalent" version = "1.0.1" @@ -1044,6 +1122,12 @@ version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904" +[[package]] +name = "humantime" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" + [[package]] name = "hyper" version = "1.3.1" @@ -1199,6 +1283,12 @@ version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f518f335dce6725a761382244631d86cf0ccb2863413590b31338feb467f9c3" +[[package]] +name = "is_terminal_polyfill" +version = "1.70.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8478577c03552c21db0e2724ffb8986a5ce7af88107e6be5d2ee6e158c12800" + [[package]] name = "itertools" version = "0.10.5" @@ -1321,9 +1411,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.21" +version = "0.4.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c" +checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" [[package]] name = "lru-cache" @@ -2700,6 +2790,8 @@ dependencies = [ name = "trade-bot" version = "0.1.0" dependencies = [ + "env_logger", + "log", "serde", "tokio", "toml", @@ -2790,6 +2882,12 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + [[package]] name = "uuid" version = "1.8.0" diff --git a/Cargo.toml b/Cargo.toml index 212c95d..04f99d2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,6 +11,8 @@ veloren-client = { git = "https://gitlab.com/veloren/veloren", branch = "master" veloren-world = { git = "https://gitlab.com/veloren/veloren", branch = "master" } toml = "0.8.14" serde = { version = "1.0.203", features = ["derive"] } +log = "0.4.22" +env_logger = "0.11.3" [patch.crates-io] specs = { git = "https://github.com/amethyst/specs.git", rev = "4e2da1df29ee840baa9b936593c45592b7c9ae27" } diff --git a/src/bot.rs b/src/bot.rs index 4e5f0cc..4a9e627 100644 --- a/src/bot.rs +++ b/src/bot.rs @@ -1,47 +1,29 @@ use std::{ - borrow::{Borrow, BorrowMut}, collections::HashMap, - sync::{ - mpsc::{self, Receiver, Sender}, - Arc, Mutex, - }, - thread, - time::{self, Duration, Instant, UNIX_EPOCH}, + sync::Arc, + time::{Duration, Instant}, }; -use serde::Serialize; use tokio::runtime::Runtime; use veloren_client::{addr::ConnectionArgs, Client, Event as VelorenEvent, WorldExt}; use veloren_common::{ clock::Clock, - comp::{ - self, - character_state::CharacterStateEventEmitters, - invite::InviteKind, - item::{self, ItemDefinitionId, ItemDefinitionIdOwned, ItemDesc}, - CharacterState, ChatType, Controller, ControllerInputs, InputKind, Item, Pos, - }, - event::{EmitExt, EventBus}, + comp::{invite::InviteKind, item::ItemDefinitionIdOwned, ChatType, ControllerInputs, Pos}, outcome::Outcome, - trade::{PendingTrade, TradeAction, TradePhase}, + trade::{PendingTrade, TradeAction}, uid::Uid, ViewDistances, }; -use veloren_common_net::{ - msg::{InviteAnswer, Notification}, - sync::WorldSyncExt, -}; +use veloren_common_net::sync::WorldSyncExt; const COINS: &str = "common.items.utility.coins"; enum TradeMode { Take, - Buy, - Sell, + Trade, } pub struct Bot { - username: String, position: [f32; 3], client: Client, clock: Clock, @@ -49,6 +31,7 @@ pub struct Bot { sell_prices: HashMap, last_action: Instant, last_announcement: Instant, + is_player_notified: bool, trade_mode: TradeMode, } @@ -60,11 +43,12 @@ impl Bot { sell_prices: HashMap, position: [f32; 3], ) -> Result { + log::info!("Connecting to veloren"); + let client = connect_to_veloren(&username, password)?; let clock = Clock::new(Duration::from_secs_f64(1.0 / 60.0)); Ok(Bot { - username, position, client, clock, @@ -72,11 +56,14 @@ impl Bot { sell_prices, last_action: Instant::now(), last_announcement: Instant::now(), - trade_mode: TradeMode::Buy, + is_player_notified: false, + trade_mode: TradeMode::Trade, }) } pub fn select_character(&mut self) -> Result<(), String> { + log::info!("Selecting a character"); + self.client.load_character_list(); while self.client.character_list().loading { @@ -103,6 +90,7 @@ impl Bot { entity: 4, }, ); + self.client.sort_inventory(); Ok(()) } @@ -131,28 +119,33 @@ impl Bot { let entity = self.client.entity().clone(); let mut position_state = self.client.state_mut().ecs().write_storage::(); - position_state.insert(entity, Pos(self.position.into())); + position_state + .insert(entity, Pos(self.position.into())) + .map_err(|error| error.to_string())?; } } if let Some((_, trade, _)) = self.client.pending_trade() { match self.trade_mode { - TradeMode::Buy => self.handle_buy(trade.clone())?, + TradeMode::Trade => self.handle_trade(trade.clone())?, TradeMode::Take => self.handle_take(trade.clone()), - TradeMode::Sell => self.handle_sell(trade.clone())?, } } + if !self.client.is_trading() { + self.trade_mode = TradeMode::Trade; + self.is_player_notified = false; + + self.client.accept_invite(); + } + self.last_action = Instant::now(); } if self.last_announcement.elapsed() > Duration::from_secs(600) { self.client.send_command( "region".to_string(), - vec![ - "I'm a bot. Use /say or /tell to give commands: 'buy', 'sell' or 'prices'." - .to_string(), - ], + vec!["I'm a bot. Trade with me or say 'prices' to see my offers.".to_string()], ); self.last_announcement = Instant::now(); @@ -170,44 +163,13 @@ impl Bot { match message.chat_type { ChatType::Tell(sender_uid, _) | ChatType::Say(sender_uid) => { - if !self.client.is_trading() { - match content.trim() { - "buy" => { - self.trade_mode = TradeMode::Buy; - self.client.send_invite(sender_uid, InviteKind::Trade); - } - "sell" => { - self.trade_mode = TradeMode::Sell; - self.client.send_invite(sender_uid, InviteKind::Trade); - } - "take" => { + match content.trim() { + "prices" => self.send_price_info(&sender_uid)?, + "take" => { + if !self.client.is_trading() { self.trade_mode = TradeMode::Take; self.client.send_invite(sender_uid, InviteKind::Trade); } - _ => {} - } - } - match content.trim() { - "prices" => { - let player_name = self - .find_name(&sender_uid) - .ok_or("Failed to find player name")? - .to_string(); - - self.client.send_command( - "tell".to_string(), - vec![ - player_name.clone(), - format!("Buy prices: {:?}", self.buy_prices), - ], - ); - self.client.send_command( - "tell".to_string(), - vec![ - player_name, - format!("Sell prices: {:?}", self.sell_prices), - ], - ); } _ => {} } @@ -217,11 +179,8 @@ impl Bot { } } VelorenEvent::Outcome(Outcome::ProjectileHit { - pos, - body, - vel, - source, target: Some(target), + .. }) => { if let Some(uid) = self.client.uid() { if uid == target { @@ -236,16 +195,20 @@ impl Bot { Ok(()) } - fn handle_buy(&mut self, trade: PendingTrade) -> Result<(), String> { - let (my_offer, their_offer) = { - let my_offer_index = trade - .which_party(self.client.uid().ok_or("Failed to get uid")?) - .ok_or("Failed to get offer index")?; - let their_offer_index = if my_offer_index == 0 { 1 } else { 0 }; - + fn handle_trade(&mut self, trade: PendingTrade) -> Result<(), String> { + let my_offer_index = trade + .which_party(self.client.uid().ok_or("Failed to get uid")?) + .ok_or("Failed to get offer index")?; + let their_offer_index = if my_offer_index == 0 { 1 } else { 0 }; + let (my_offer, their_offer, them) = { ( &trade.offers[my_offer_index], &trade.offers[their_offer_index], + self.client + .state() + .ecs() + .entity_from_uid(trade.parties[their_offer_index]) + .ok_or("Failed to find player".to_string())?, ) }; let inventories = self.client.inventories(); @@ -253,225 +216,204 @@ impl Bot { let my_coins = my_inventory .get_slot_of_item_by_def_id(&ItemDefinitionIdOwned::Simple(COINS.to_string())) .ok_or("Failed to find coins".to_string())?; - let them = self - .client - .state() - .ecs() - .entity_from_uid(trade.parties[1]) - .ok_or("Failed to find player".to_string())?; - let their_inventory = inventories - .get(them) - .ok_or("Failed to find inventory".to_string())?; - let their_offered_items_value = - their_offer.into_iter().fold(0, |acc, (slot_id, quantity)| { - if let Some(item) = their_inventory.get(slot_id.clone()) { - let item_value = self - .buy_prices - .get(&item.persistence_item_id()) - .unwrap_or(&1); - - acc + (item_value * quantity) - } else { - acc - } - }); - let my_offered_coins = my_offer - .into_iter() - .find_map(|(slot_id, quantity)| { - let item = if let Some(item) = my_inventory.get(slot_id.clone()) { - item - } else { - return None; - }; - - if item.item_definition_id() == ItemDefinitionId::Simple(COINS.into()) { - Some(quantity) - } else { - None - } - }) - .unwrap_or(&0); - let difference: i32 = their_offered_items_value as i32 - *my_offered_coins as i32; - - let mut my_items_to_remove = Vec::new(); - - for (item_id, quantity) in my_offer { - let item = my_inventory - .get(item_id.clone()) - .ok_or("Failed to find item".to_string())?; - - if item.item_definition_id() != ItemDefinitionId::Simple(COINS.into()) { - my_items_to_remove.push((item_id.clone(), *quantity)); - } - } - - let mut their_items_to_remove = Vec::new(); - - for (item_id, quantity) in their_offer { - let item = their_inventory - .get(item_id.clone()) - .ok_or("Failed to find item".to_string())?; - - if !self.buy_prices.contains_key(&item.persistence_item_id()) { - their_items_to_remove.push((item_id.clone(), *quantity)); - } - } - - drop(inventories); - - for (item, quantity) in my_items_to_remove { - self.client.perform_trade_action(TradeAction::RemoveItem { - item, - quantity, - ours: true, - }); - } - - for (item, quantity) in their_items_to_remove { - self.client.perform_trade_action(TradeAction::RemoveItem { - item, - quantity, - ours: false, - }); - } - - if difference == 0 { - self.client - .perform_trade_action(TradeAction::Accept(trade.phase)); - } else if difference.is_positive() { - self.client.perform_trade_action(TradeAction::AddItem { - item: my_coins, - quantity: difference as u32, - ours: true, - }); - } else if difference.is_negative() { - self.client.perform_trade_action(TradeAction::RemoveItem { - item: my_coins, - quantity: difference.abs() as u32, - ours: true, - }); - } - - Ok(()) - } - - fn handle_sell(&mut self, trade: PendingTrade) -> Result<(), String> { - let (my_offer, their_offer) = { - let my_offer_index = trade - .which_party(self.client.uid().ok_or("Failed to get uid")?) - .ok_or("Failed to get offer index")?; - let their_offer_index = if my_offer_index == 0 { 1 } else { 0 }; - - ( - &trade.offers[my_offer_index], - &trade.offers[their_offer_index], - ) - }; - let inventories = self.client.inventories(); - let my_inventory = inventories.get(self.client.entity()).unwrap(); - let them = self - .client - .state() - .ecs() - .entity_from_uid(trade.parties[1]) - .ok_or("Failed to find player".to_string())?; let their_inventory = inventories .get(them) .ok_or("Failed to find inventory".to_string())?; let their_coins = their_inventory .get_slot_of_item_by_def_id(&ItemDefinitionIdOwned::Simple(COINS.to_string())) .ok_or("Failed to find coins")?; - let my_offered_items_value = my_offer.into_iter().fold(0, |acc, (slot_id, quantity)| { - if let Some(item) = my_inventory.get(slot_id.clone()) { - let item_value = self - .sell_prices - .get(&item.persistence_item_id()) - .unwrap_or(&0); + let their_total_coin_amount = their_inventory + .get(their_coins) + .map(|item| item.amount() as i32) + .unwrap_or(0); + let (mut their_offered_coin_amount, mut my_offered_coin_amount) = (0, 0); + let their_offered_items_value = + their_offer + .into_iter() + .fold(0, |acc: i32, (slot_id, quantity)| { + if let Some(item) = their_inventory.get(slot_id.clone()) { + let item_id = item.persistence_item_id(); - acc + (item_value * quantity) - } else { - acc - } - }); - let their_offered_coins = their_offer - .into_iter() - .find_map(|(slot_id, quantity)| { - let item = if let Some(item) = their_inventory.get(slot_id.clone()) { - item - } else { - return None; - }; + let item_value = if item_id == COINS { + their_offered_coin_amount = *quantity as i32; - if item.item_definition_id() - == ItemDefinitionId::Simple("common.items.utility.coins".into()) - { - Some(quantity) - } else { - None - } - }) - .unwrap_or(&0); - let difference: i32 = my_offered_items_value as i32 - *their_offered_coins as i32; + 1 + } else { + self.buy_prices + .get(&item_id) + .map(|int| *int as i32) + .unwrap_or_else(|| { + self.sell_prices + .get(&item_id) + .map(|int| 0 - *int as i32) + .unwrap_or(0) + }) + }; - let mut their_items_to_remove = Vec::new(); + acc.saturating_add(item_value.saturating_mul(*quantity as i32)) + } else { + acc + } + }); + let my_offered_items_value = + my_offer + .into_iter() + .fold(0, |acc: i32, (slot_id, quantity)| { + if let Some(item) = my_inventory.get(slot_id.clone()) { + let item_id = item.persistence_item_id(); - for (item_id, quantity) in their_offer { - let item = their_inventory - .get(item_id.clone()) - .ok_or("Failed to find item".to_string())?; + let item_value = if item_id == COINS { + my_offered_coin_amount = *quantity as i32; - if item.item_definition_id() - != ItemDefinitionId::Simple("common.items.utility.coins".into()) - { - their_items_to_remove.push((item_id.clone(), *quantity)); - } - } + 1 + } else { + self.sell_prices + .get(&item_id) + .map(|int| *int as i32) + .unwrap_or_else(|| { + self.buy_prices + .get(&item_id) + .map(|int| 0 - *int as i32) + .unwrap_or(i32::MAX) + }) + }; + + acc.saturating_add(item_value.saturating_mul(*quantity as i32)) + } else { + acc + } + }); let mut my_items_to_remove = Vec::new(); - for (item_id, quantity) in my_offer { + for (slot_id, amount) in my_offer { let item = my_inventory - .get(item_id.clone()) - .ok_or("Failed to find item".to_string())?; + .get(slot_id.clone()) + .ok_or("Failed to get item")?; + let item_id = item.persistence_item_id(); - if !self.sell_prices.contains_key(&item.persistence_item_id()) { - my_items_to_remove.push((item_id.clone(), *quantity)); + if item_id == COINS { + continue; + } + + if !self.sell_prices.contains_key(&item_id) { + my_items_to_remove.push((slot_id.clone(), *amount)); + } + } + + let mut their_items_to_remove = Vec::new(); + + for (slot_id, amount) in their_offer { + let item = their_inventory + .get(slot_id.clone()) + .ok_or("Failed to get item")?; + + let item_id = item.persistence_item_id(); + + if item_id == COINS { + continue; + } + + if !self.buy_prices.contains_key(&item_id) { + their_items_to_remove.push((slot_id.clone(), *amount)); } } drop(inventories); - for (item, quantity) in their_items_to_remove { - self.client.perform_trade_action(TradeAction::RemoveItem { - item, - quantity, - ours: false, - }); + if !self.is_player_notified { + self.send_price_info(&trade.parties[their_offer_index])?; + + self.is_player_notified = true; } - for (item, quantity) in my_items_to_remove { - self.client.perform_trade_action(TradeAction::RemoveItem { - item, - quantity, - ours: true, - }); + if their_offered_items_value == 0 && my_offered_items_value == 0 { + return Ok(()); } + if !my_items_to_remove.is_empty() { + for (item, quantity) in my_items_to_remove { + self.client.perform_trade_action(TradeAction::RemoveItem { + item, + quantity, + ours: true, + }); + } + + return Ok(()); + } + + if !their_items_to_remove.is_empty() { + for (item, quantity) in their_items_to_remove { + self.client.perform_trade_action(TradeAction::RemoveItem { + item, + quantity, + ours: false, + }); + } + + return Ok(()); + } + + if my_offered_items_value > their_total_coin_amount { + self.client.send_command( + "tell".to_string(), + vec![ + self.find_name(&trade.parties[their_offer_index]) + .ok_or("Failed to get uid")? + .to_string(), + format!("I need {my_offered_items_value} coins or trade value from you."), + ], + ); + + return Ok(()); + } + + let difference: i32 = their_offered_items_value as i32 - my_offered_items_value as i32; + + // If the trade is balanced if difference == 0 { + // Accept self.client .perform_trade_action(TradeAction::Accept(trade.phase)); + // If they are offering more } else if difference.is_positive() { - self.client.perform_trade_action(TradeAction::AddItem { - item: their_coins, - quantity: difference as u32, - ours: false, - }); + // If they are offering coins + if their_offered_coin_amount > 0 { + // Remove their coins to balance + self.client.perform_trade_action(TradeAction::RemoveItem { + item: their_coins, + quantity: difference as u32, + ours: false, + }); + // If they are not offering coins + } else { + // Add my coins to balanace + self.client.perform_trade_action(TradeAction::AddItem { + item: my_coins, + quantity: difference as u32, + ours: true, + }); + } + // If I am offering more } else if difference.is_negative() { - self.client.perform_trade_action(TradeAction::RemoveItem { - item: their_coins, - quantity: difference.abs() as u32, - ours: false, - }); + // If I am offering coins + if my_offered_coin_amount > 0 { + // Remove my coins to balance + self.client.perform_trade_action(TradeAction::RemoveItem { + item: my_coins, + quantity: difference.abs() as u32, + ours: true, + }); + // If I am not offering coins + } else { + // Add their coins to balance + self.client.perform_trade_action(TradeAction::AddItem { + item: their_coins, + quantity: difference.abs() as u32, + ours: false, + }); + } } Ok(()) @@ -484,6 +426,27 @@ impl Bot { } } + fn send_price_info(&mut self, target: &Uid) -> Result<(), String> { + let player_name = self + .find_name(target) + .ok_or("Failed to find player name")? + .to_string(); + + self.client.send_command( + "tell".to_string(), + vec![ + player_name.clone(), + format!("Buy prices: {:?}", self.buy_prices), + ], + ); + self.client.send_command( + "tell".to_string(), + vec![player_name, format!("Sell prices: {:?}", self.sell_prices)], + ); + + Ok(()) + } + fn find_name<'a>(&'a self, uid: &Uid) -> Option<&'a String> { self.client.player_list().iter().find_map(|(id, info)| { if id == uid { @@ -495,7 +458,7 @@ impl Bot { }) } - fn find_uid<'a>(&'a self, name: &str) -> Option<&'a Uid> { + fn _find_uid<'a>(&'a self, name: &str) -> Option<&'a Uid> { self.client.player_list().iter().find_map(|(id, info)| { if info.player_alias == name { Some(id) @@ -505,7 +468,7 @@ impl Bot { }) } - fn find_uuid(&self, name: &str) -> Option { + fn _find_uuid(&self, name: &str) -> Option { self.client.player_list().iter().find_map(|(_, info)| { if info.player_alias == name { Some(info.uuid.to_string()) diff --git a/src/main.rs b/src/main.rs index fc88b23..98290b9 100644 --- a/src/main.rs +++ b/src/main.rs @@ -4,9 +4,6 @@ use std::{ collections::HashMap, env::var, fs::{read_to_string, write}, - sync::{Arc, Mutex}, - thread::{sleep, spawn}, - time::Duration, }; use bot::Bot; @@ -29,7 +26,7 @@ impl Config { toml::from_str::(&config_file_content).map_err(|error| error.to_string()) } - fn write(&self) -> Result<(), String> { + fn _write(&self) -> Result<(), String> { let config_path = var("CONFIG_PATH").map_err(|error| error.to_string())?; let config_string = toml::to_string(self).map_err(|error| error.to_string())?; @@ -38,6 +35,8 @@ impl Config { } fn main() { + env_logger::init(); + let config = Config::read().unwrap(); let mut bot = Bot::new( config.username, @@ -51,6 +50,6 @@ fn main() { bot.select_character().expect("Failed to select character"); loop { - bot.tick().inspect_err(|error| eprintln!("{error}")); + let _ = bot.tick().inspect_err(|error| eprintln!("{error}")); } }