trade_bot/src/bot.rs

933 lines
34 KiB
Rust
Raw Normal View History

2024-07-13 04:37:16 +00:00
/**
A bot that buys, sells and trades with players.
This bot is designed to run on the official Veloren server. It will connect to the server, select
a character, and then attempt to buy, sell, and trade with other players. The bot will also
announce its presence and respond to chat messages.
See [main.rs] for an example of how to run this bot.
**/
2024-07-03 23:04:49 +00:00
use std::{
2024-07-16 09:31:24 +00:00
borrow::Cow,
2024-07-07 08:52:25 +00:00
sync::Arc,
time::{Duration, Instant},
2024-07-03 23:04:49 +00:00
};
use hashbrown::HashMap;
2024-07-03 23:04:49 +00:00
use tokio::runtime::Runtime;
use vek::Quaternion;
use veloren_client::{addr::ConnectionArgs, Client, Event as VelorenEvent, SiteInfoRich, WorldExt};
2024-07-16 09:31:24 +00:00
use veloren_client_i18n::LocalizationHandle;
2024-07-03 23:04:49 +00:00
use veloren_common::{
clock::Clock,
2024-07-16 09:31:24 +00:00
comp::{
invite::InviteKind,
item::{ItemDefinitionId, ItemDefinitionIdOwned, ItemDesc, ItemI18n, MaterialStatManifest},
2024-07-16 11:51:19 +00:00
slot::InvSlotId,
2024-07-16 09:31:24 +00:00
tool::AbilityMap,
ChatType, ControllerInputs, Item, Ori, Pos,
},
2024-07-04 18:40:07 +00:00
outcome::Outcome,
2024-07-08 17:59:35 +00:00
time::DayPeriod,
2024-07-08 20:39:26 +00:00
trade::{PendingTrade, TradeAction, TradeResult},
2024-07-03 23:04:49 +00:00
uid::Uid,
uuid::Uuid,
DamageSource, ViewDistances,
2024-07-03 23:04:49 +00:00
};
2024-07-07 08:52:25 +00:00
use veloren_common_net::sync::WorldSyncExt;
2024-07-03 23:04:49 +00:00
2024-07-04 05:19:32 +00:00
const COINS: &str = "common.items.utility.coins";
2024-07-13 04:37:16 +00:00
/// An active connection to the Veloren server that will attempt to run every time the `tick`
/// function is called.
///
/// See the [module-level documentation](index.html) for more information.
2024-07-03 23:04:49 +00:00
pub struct Bot {
2024-07-11 06:58:52 +00:00
username: String,
2024-07-04 18:40:07 +00:00
position: [f32; 3],
orientation: f32,
admins: Vec<String>,
2024-07-13 07:51:37 +00:00
announcement: Option<String>,
2024-07-08 19:15:33 +00:00
2024-07-03 23:04:49 +00:00
client: Client,
clock: Clock,
2024-07-16 09:31:24 +00:00
ability_map: AbilityMap,
material_manifest: MaterialStatManifest,
item_i18n: ItemI18n,
localization: LocalizationHandle,
2024-07-08 19:15:33 +00:00
2024-07-13 03:54:44 +00:00
buy_prices: HashMap<String, u32>,
sell_prices: HashMap<String, u32>,
2024-07-08 19:15:33 +00:00
trade_mode: TradeMode,
2024-07-16 11:51:19 +00:00
previous_offer: Option<(HashMap<InvSlotId, u32>, HashMap<InvSlotId, u32>)>,
last_trade_action: Instant,
2024-07-06 01:48:26 +00:00
last_announcement: Instant,
2024-07-08 19:15:33 +00:00
last_ouch: Instant,
2024-07-11 18:30:50 +00:00
sort_count: u8,
2024-07-03 23:04:49 +00:00
}
impl Bot {
/// Connect to the official veloren server, select the specified character
/// and return a Bot instance ready to run.
2024-07-13 03:54:44 +00:00
#[allow(clippy::too_many_arguments)]
2024-07-04 05:19:32 +00:00
pub fn new(
2024-07-11 06:58:52 +00:00
username: String,
2024-07-04 05:19:32 +00:00
password: &str,
character: &str,
admins: Vec<String>,
2024-07-13 03:54:44 +00:00
buy_prices: HashMap<String, u32>,
sell_prices: HashMap<String, u32>,
2024-07-04 18:40:07 +00:00
position: [f32; 3],
orientation: f32,
2024-07-13 07:51:37 +00:00
announcement: Option<String>,
2024-07-04 05:19:32 +00:00
) -> Result<Self, String> {
2024-07-07 08:52:25 +00:00
log::info!("Connecting to veloren");
2024-07-11 06:58:52 +00:00
let mut client = connect_to_veloren(&username, password)?;
let mut clock = Clock::new(Duration::from_secs_f64(1.0 / 30.0));
2024-07-03 23:04:49 +00:00
client.load_character_list();
2024-07-03 23:04:49 +00:00
while client.character_list().loading {
client
.tick(ControllerInputs::default(), clock.dt())
2024-07-03 23:04:49 +00:00
.map_err(|error| format!("{error:?}"))?;
clock.tick();
2024-07-03 23:04:49 +00:00
}
let character_id = client
2024-07-03 23:04:49 +00:00
.character_list()
.characters
.iter()
.find(|character_item| character_item.character.alias == character)
2024-07-13 03:54:44 +00:00
.ok_or_else(|| format!("No character named {character}"))?
2024-07-03 23:04:49 +00:00
.character
.id
2024-07-13 03:54:44 +00:00
.ok_or("Failed to get character ID")?;
2024-07-03 23:04:49 +00:00
2024-07-16 09:31:24 +00:00
log::info!("Selecting a character");
// This loop waits and retries requesting the character in the case that the character has
2024-07-16 11:51:19 +00:00
// logged out too recently.
while client.position().is_none() {
client.request_character(
character_id,
ViewDistances {
terrain: 4,
entity: 4,
},
);
client
.tick(ControllerInputs::default(), clock.dt())
.map_err(|error| format!("{error:?}"))?;
clock.tick();
}
2024-07-03 23:04:49 +00:00
let now = Instant::now();
Ok(Bot {
2024-07-11 06:58:52 +00:00
username,
position,
orientation,
admins,
client,
clock,
2024-07-16 09:31:24 +00:00
ability_map: AbilityMap::load().read().clone(),
material_manifest: MaterialStatManifest::load().read().clone(),
item_i18n: ItemI18n::new_expect(),
localization: LocalizationHandle::load_expect("en"),
buy_prices,
sell_prices,
trade_mode: TradeMode::Trade,
2024-07-13 05:39:18 +00:00
previous_offer: None,
last_trade_action: now,
last_announcement: now,
last_ouch: now,
2024-07-11 18:30:50 +00:00
sort_count: 0,
announcement,
})
2024-07-03 23:04:49 +00:00
}
2024-07-13 04:37:16 +00:00
/// Run the bot for a single tick. This should be called in a loop.
///
2024-07-13 07:51:37 +00:00
/// There are three timers in this function:
/// - The [Clock] runs the Veloren client. At **30 ticks per second** this timer is faster than
/// the others so the bot can respond to events quickly.
/// - `last_trade_action` times the bot's behavior to compensate for latency, every **300ms**.
/// - `last_announcement` times the bot's announcements to **45 minutes** and is checked while
/// processing trade actions.
///
2024-07-13 04:37:16 +00:00
/// This function should be modified with care. In addition to being the bot's main loop, it
/// also accepts incoming trade invites, which has a potential for error if the bot accepts an
/// invite while in the wrong trade mode.
2024-07-03 23:04:49 +00:00
pub fn tick(&mut self) -> Result<(), String> {
2024-07-04 05:19:32 +00:00
let veloren_events = self
2024-07-03 23:04:49 +00:00
.client
.tick(ControllerInputs::default(), self.clock.dt())
.map_err(|error| format!("{error:?}"))?;
2024-07-04 05:19:32 +00:00
for event in veloren_events {
self.handle_veloren_event(event)?;
2024-07-03 23:04:49 +00:00
}
if self.last_trade_action.elapsed() > Duration::from_millis(300) {
2024-07-13 07:51:37 +00:00
self.client.respawn();
2024-07-08 14:39:19 +00:00
self.handle_position_and_orientation()?;
2024-07-08 18:09:15 +00:00
self.handle_lantern();
2024-07-04 18:40:07 +00:00
2024-07-04 05:19:32 +00:00
if let Some((_, trade, _)) = self.client.pending_trade() {
match self.trade_mode {
TradeMode::AdminAccess => {
if !trade.is_empty_trade() {
self.client
.perform_trade_action(TradeAction::Accept(trade.phase));
}
}
2024-07-07 08:52:25 +00:00
TradeMode::Trade => self.handle_trade(trade.clone())?,
2024-07-04 05:19:32 +00:00
}
} else if self.client.pending_invites().is_empty() {
match self.trade_mode {
TradeMode::AdminAccess => {
2024-07-16 02:59:30 +00:00
// This should never happen, but in case the server fails to send a trade
// invite, the bot will switch to trade mode.
self.trade_mode = TradeMode::Trade;
}
TradeMode::Trade => {
self.client.accept_invite();
}
}
2024-07-07 08:52:25 +00:00
}
2024-07-13 07:51:37 +00:00
if self.sort_count > 0 {
self.client.sort_inventory();
self.sort_count -= 1;
if self.sort_count == 0 {
log::info!("Sorted inventory, finished")
} else {
log::info!("Sorted inventory, {} more times to go", self.sort_count);
}
}
if self.last_announcement.elapsed() > Duration::from_mins(45) {
self.handle_announcement()?;
2024-07-06 01:48:26 +00:00
2024-07-08 19:15:33 +00:00
self.last_announcement = Instant::now();
}
2024-07-12 15:15:50 +00:00
self.last_trade_action = Instant::now();
2024-07-06 01:48:26 +00:00
}
2024-07-03 23:04:49 +00:00
self.clock.tick();
Ok(())
}
2024-07-15 18:47:51 +00:00
/// Consume and manage a client-side Veloren event.
2024-07-04 05:19:32 +00:00
fn handle_veloren_event(&mut self, event: VelorenEvent) -> Result<(), String> {
2024-07-04 18:40:07 +00:00
match event {
VelorenEvent::Chat(message) => {
2024-07-11 06:58:52 +00:00
let sender = if let ChatType::Tell(uid, _) = message.chat_type {
uid
} else {
return Ok(());
2024-07-09 00:16:10 +00:00
};
2024-07-04 18:40:07 +00:00
let content = message.content().as_plain().unwrap_or_default();
2024-07-11 03:57:25 +00:00
let mut split_content = content.split(' ');
let command = split_content.next().unwrap_or_default();
2024-07-12 15:59:28 +00:00
let price_correction_message = "Use the format 'price [search_term]'";
let correction_message = match command {
"admin_access" => {
if self.is_user_admin(&sender)? && !self.client.is_trading() {
log::info!("Providing admin access");
self.previous_offer = None;
self.trade_mode = TradeMode::AdminAccess;
2024-07-13 07:51:37 +00:00
self.client.send_invite(sender, InviteKind::Trade);
2024-07-12 15:59:28 +00:00
None
} else {
Some(price_correction_message)
2024-07-11 03:57:25 +00:00
}
2024-07-03 23:04:49 +00:00
}
"announce" => {
if self.is_user_admin(&sender)? {
self.handle_announcement()?;
2024-07-13 05:39:18 +00:00
self.last_announcement = Instant::now();
2024-07-12 15:59:28 +00:00
None
} else {
Some(price_correction_message)
2024-07-09 00:16:10 +00:00
}
}
"ori" => {
if self.is_user_admin(&sender)? {
if let Some(orientation) = split_content.next() {
self.orientation = orientation
.parse::<f32>()
.map_err(|error| error.to_string())?;
None
} else {
Some("Use the format 'ori [0-360]'")
}
} else {
Some(price_correction_message)
}
}
"price" => {
for item_name in split_content {
self.send_price_info(&sender, &item_name)?;
}
None
}
"pos" => {
2024-07-11 17:18:55 +00:00
if self.is_user_admin(&sender)? {
if let (Some(x), Some(y), Some(z)) = (
split_content.next(),
split_content.next(),
split_content.next(),
) {
let position = [
x.parse::<f32>().map_err(|error| error.to_string())?,
y.parse::<f32>().map_err(|error| error.to_string())?,
z.parse::<f32>().map_err(|error| error.to_string())?,
];
self.position = position;
2024-07-12 15:59:28 +00:00
None
} else {
Some("Use the format 'pos [x] [y] [z]'.")
2024-07-11 17:18:55 +00:00
}
2024-07-12 15:59:28 +00:00
} else {
Some(price_correction_message)
2024-07-11 17:18:55 +00:00
}
}
"sort" => {
2024-07-12 15:59:28 +00:00
if self.is_user_admin(&sender)? {
if let Some(sort_count) = split_content.next() {
let sort_count = sort_count
.parse::<u8>()
2024-07-12 15:59:28 +00:00
.map_err(|error| error.to_string())?;
2024-07-11 03:57:25 +00:00
log::info!("Sorting inventory {sort_count} times");
self.sort_count = sort_count;
2024-07-12 15:59:28 +00:00
} else {
self.client.sort_inventory();
log::info!("Sorting inventory once");
2024-07-12 15:59:28 +00:00
}
None
2024-07-12 15:59:28 +00:00
} else {
Some(price_correction_message)
}
}
_ => Some(price_correction_message),
};
if let Some(message) = correction_message {
2024-07-11 03:57:25 +00:00
let player_name = self
2024-07-16 09:37:07 +00:00
.find_player_alias(&sender)
2024-07-11 03:57:25 +00:00
.ok_or("Failed to find player name")?
.to_string();
self.client.send_command(
"tell".to_string(),
2024-07-12 15:59:28 +00:00
vec![player_name.clone(), message.to_string()],
2024-07-11 03:57:25 +00:00
);
}
2024-07-03 23:04:49 +00:00
}
2024-07-04 18:40:07 +00:00
VelorenEvent::Outcome(Outcome::ProjectileHit {
target: Some(target),
2024-07-07 08:52:25 +00:00
..
2024-07-04 18:40:07 +00:00
}) => {
if let Some(uid) = self.client.uid() {
2024-07-13 07:51:37 +00:00
if uid == target && self.last_ouch.elapsed() > Duration::from_secs(2) {
2024-07-04 18:40:07 +00:00
self.client
2024-07-08 19:15:33 +00:00
.send_command("say".to_string(), vec!["Ouch!".to_string()]);
self.last_ouch = Instant::now();
2024-07-04 18:40:07 +00:00
}
}
}
2024-07-08 20:39:26 +00:00
VelorenEvent::Outcome(Outcome::HealthChange { info, .. }) => {
if let Some(DamageSource::Buff(_)) = info.cause {
return Ok(());
}
2024-07-08 20:39:26 +00:00
if let Some(uid) = self.client.uid() {
if uid == info.target
2024-07-08 23:06:41 +00:00
&& info.amount.is_sign_negative()
&& self.last_ouch.elapsed() > Duration::from_secs(1)
2024-07-08 20:39:26 +00:00
{
self.client
.send_command("say".to_string(), vec!["That hurt!".to_string()]);
self.last_ouch = Instant::now();
}
}
}
VelorenEvent::TradeComplete { result, trade } => {
let my_party = trade
.which_party(self.client.uid().ok_or("Failed to find uid")?)
.ok_or("Failed to find trade party")?;
let their_party = if my_party == 0 { 1 } else { 0 };
let their_uid = trade.parties[their_party];
2024-07-16 02:59:30 +00:00
let their_name = self
2024-07-16 09:37:07 +00:00
.find_player_alias(&their_uid)
2024-07-16 02:59:30 +00:00
.ok_or("Failed to find name")?
.clone();
2024-07-13 04:37:16 +00:00
match result {
TradeResult::Completed => {
2024-07-13 05:39:18 +00:00
if let Some(offer) = &self.previous_offer {
log::info!("Trade with {their_name}: {offer:?}",);
}
2024-07-13 04:37:16 +00:00
self.client.send_command(
"say".to_string(),
vec!["Thank you for trading with me!".to_string()],
);
}
TradeResult::Declined => log::info!("Trade with {their_name} declined"),
TradeResult::NotEnoughSpace => {
log::info!("Trade with {their_name} failed: not enough space")
}
}
2024-07-08 14:39:19 +00:00
if let TradeMode::AdminAccess = self.trade_mode {
2024-07-16 02:59:30 +00:00
log::info!("End of admin access for {their_name}");
2024-07-13 05:39:18 +00:00
2024-07-09 00:16:10 +00:00
self.trade_mode = TradeMode::Trade;
2024-07-08 14:39:19 +00:00
}
}
2024-07-04 18:40:07 +00:00
_ => (),
2024-07-03 23:04:49 +00:00
}
Ok(())
}
2024-07-15 18:47:51 +00:00
/// Make the bot's trading and help accouncements
///
/// Currently, this can make two announcements: one in /region with basic usage instructions
/// is always made. If an announcement was provided when the bot was created, it will make it
/// in /world.
fn handle_announcement(&mut self) -> Result<(), String> {
log::info!("Making an announcement");
self.client.send_command(
"region".to_string(),
vec![format!(
"I'm a bot. You can trade with me or check prices: '/tell {} price [search_term]'.",
self.username
)],
);
2024-07-13 07:51:37 +00:00
if let Some(announcement) = &self.announcement {
let location = self
.client
.sites()
.into_iter()
.find_map(|(_, SiteInfoRich { site, .. })| {
let x_difference = self.position[0] - site.wpos[0] as f32;
let y_difference = self.position[1] - site.wpos[1] as f32;
if x_difference.abs() < 100.0 && y_difference.abs() < 100.0 {
site.name.clone()
} else {
None
}
})
.unwrap_or(format!("{:?}", self.position));
let interpolated_announcement = announcement.replace("{location}", &location);
self.client
.send_command("world".to_string(), vec![interpolated_announcement]);
}
Ok(())
}
2024-07-13 04:37:16 +00:00
/// Use the lantern at night and put it away during the day.
2024-07-08 17:59:35 +00:00
fn handle_lantern(&mut self) {
let day_period = self.client.state().get_day_period();
match day_period {
DayPeriod::Night => {
if !self.client.is_lantern_enabled() {
self.client.enable_lantern();
}
}
DayPeriod::Morning | DayPeriod::Noon | DayPeriod::Evening => {
if self.client.is_lantern_enabled() {
self.client.disable_lantern();
}
}
}
}
2024-07-13 04:37:16 +00:00
/// Manage an active trade.
///
/// This is a rather complex function that should be modified with care. The bot uses its buy
/// and sell prices to determine an item's value and determines the total value of each side of
2024-07-13 04:37:16 +00:00
/// the trade. Coins are hard-coded to have a value of 1 each.
///
/// The bot's trading logic is as follows:
2024-07-13 11:56:33 +00:00
///
/// 1. If the trade is empty or hasn't changed, do nothing.
2024-07-13 04:37:16 +00:00
/// 2. If my offer includes items I am not selling, remove those items unless they are coins.
/// 3. If their offer includes items I am not buying, remove those items unless they are coins.
/// 4. If the trade is balanced, accept it.
/// 5. If the total value of their offer is greater than the total value of my offer:
/// 1. If they are offering coins, remove them to balance.
2024-07-13 04:37:16 +00:00
/// 2. If they are not offering coins, add mine to balance.
/// 6. If the total value of my offer is greater than the total value of their offer:
/// 1. If I am offering coins, remove them to balance.
2024-07-13 04:37:16 +00:00
/// 2. If I am not offering coins, add theirs to balance.
///
/// See the inline comments for more details.
2024-07-13 11:56:33 +00:00
#[allow(clippy::comparison_chain)]
2024-07-07 08:52:25 +00:00
fn handle_trade(&mut self, trade: PendingTrade) -> Result<(), String> {
2024-07-09 00:16:10 +00:00
if trade.is_empty_trade() {
return Ok(());
}
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 };
2024-07-16 11:51:19 +00:00
let (my_offer, their_offer) = {
2024-07-06 01:48:26 +00:00
(
&trade.offers[my_offer_index],
&trade.offers[their_offer_index],
)
};
// If the trade hasn't changed, do nothing to avoid spamming the server.
if let Some(previous) = &self.previous_offer {
2024-07-16 11:51:19 +00:00
if (&previous.0, &previous.1) == (my_offer, their_offer) {
return Ok(());
}
}
2024-07-16 11:51:19 +00:00
let inventories = self.client.inventories();
let me = self.client.entity();
let them = self
.client
.state()
.ecs()
.entity_from_uid(trade.parties[their_offer_index])
.ok_or("Failed to find player".to_string())?;
let (my_inventory, their_inventory) = (
inventories.get(me).ok_or("Failed to find inventory")?,
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 (mut my_offered_coin_amount, mut their_offered_coin_amount) = (0, 0);
let my_offered_items_value =
my_offer
2024-07-07 08:52:25 +00:00
.into_iter()
.fold(0, |acc: i32, (slot_id, quantity)| {
2024-07-16 11:51:19 +00:00
if let Some(item) = my_inventory.get(*slot_id) {
2024-07-07 08:52:25 +00:00
let item_id = item.persistence_item_id();
let item_value = if item_id == COINS {
2024-07-16 11:51:19 +00:00
my_offered_coin_amount = *quantity as i32;
2024-07-07 08:52:25 +00:00
1
} else {
2024-07-16 11:51:19 +00:00
self.sell_prices
2024-07-07 08:52:25 +00:00
.get(&item_id)
.map(|int| *int as i32)
.unwrap_or_else(|| {
2024-07-16 11:51:19 +00:00
self.buy_prices
2024-07-07 08:52:25 +00:00
.get(&item_id)
.map(|int| 0 - *int as i32)
2024-07-16 11:51:19 +00:00
.unwrap_or(i32::MIN)
2024-07-07 08:52:25 +00:00
})
};
acc.saturating_add(item_value.saturating_mul(*quantity as i32))
} else {
acc
}
});
2024-07-16 11:51:19 +00:00
let their_offered_items_value =
their_offer
2024-07-07 08:52:25 +00:00
.into_iter()
.fold(0, |acc: i32, (slot_id, quantity)| {
2024-07-16 11:51:19 +00:00
if let Some(item) = their_inventory.get(*slot_id) {
2024-07-07 08:52:25 +00:00
let item_id = item.persistence_item_id();
let item_value = if item_id == COINS {
2024-07-16 11:51:19 +00:00
their_offered_coin_amount = *quantity as i32;
2024-07-07 08:52:25 +00:00
1
} else {
2024-07-16 11:51:19 +00:00
self.buy_prices
2024-07-07 08:52:25 +00:00
.get(&item_id)
.map(|int| *int as i32)
.unwrap_or_else(|| {
2024-07-16 11:51:19 +00:00
self.sell_prices
2024-07-07 08:52:25 +00:00
.get(&item_id)
.map(|int| 0 - *int as i32)
2024-07-16 11:51:19 +00:00
.unwrap_or(0)
2024-07-07 08:52:25 +00:00
})
};
acc.saturating_add(item_value.saturating_mul(*quantity as i32))
} else {
acc
}
});
2024-07-03 23:04:49 +00:00
let mut my_item_to_remove = None;
2024-07-04 18:40:07 +00:00
2024-07-07 08:52:25 +00:00
for (slot_id, amount) in my_offer {
let item = my_inventory.get(*slot_id).ok_or("Failed to get item")?;
2024-07-07 08:52:25 +00:00
let item_id = item.persistence_item_id();
2024-07-04 18:40:07 +00:00
2024-07-07 08:52:25 +00:00
if item_id == COINS {
continue;
}
if !self.sell_prices.contains_key(&item_id) {
my_item_to_remove = Some((slot_id, amount));
2024-07-04 18:40:07 +00:00
}
}
let mut their_item_to_remove = None;
2024-07-04 18:40:07 +00:00
2024-07-07 08:52:25 +00:00
for (slot_id, amount) in their_offer {
let item = their_inventory.get(*slot_id).ok_or("Failed to get item")?;
2024-07-07 08:52:25 +00:00
let item_id = item.persistence_item_id();
2024-07-04 18:40:07 +00:00
2024-07-07 08:52:25 +00:00
if item_id == COINS {
continue;
}
if !self.buy_prices.contains_key(&item_id) {
their_item_to_remove = Some((slot_id, amount));
2024-07-04 18:40:07 +00:00
}
}
2024-07-13 05:39:18 +00:00
drop(inventories);
2024-07-03 23:04:49 +00:00
2024-07-16 11:51:19 +00:00
// Up until now there may have been an error, so we only update the previous offer now.
// The trade action is infallible from here.
self.previous_offer = Some((my_offer.clone(), their_offer.clone()));
// Before running any actual trade logic, remove items that are not for sale or not being
// purchased. End this trade action if an item was removed.
if let Some((slot_id, quantity)) = my_item_to_remove {
self.client.perform_trade_action(TradeAction::RemoveItem {
item: *slot_id,
quantity: *quantity,
ours: true,
});
2024-07-04 18:40:07 +00:00
2024-07-07 08:52:25 +00:00
return Ok(());
2024-07-04 18:40:07 +00:00
}
if let Some((slot_id, quantity)) = their_item_to_remove {
self.client.perform_trade_action(TradeAction::RemoveItem {
item: *slot_id,
quantity: *quantity,
ours: false,
});
2024-07-07 08:52:25 +00:00
return Ok(());
2024-07-04 18:40:07 +00:00
}
let difference = their_offered_items_value - my_offered_items_value;
2024-07-04 18:40:07 +00:00
// The if/else statements below implement the bot's main feature: buying, selling and
2024-07-16 11:51:19 +00:00
// trading items according to the values set in the configuration file. Coins are used to
// balance the value of the trade. In the case that we try to add more coins than are
// available, the server will correct it by adding all of the available coins.
2024-07-07 08:52:25 +00:00
// If the trade is balanced
2024-07-04 05:19:32 +00:00
if difference == 0 {
2024-07-07 08:52:25 +00:00
// Accept
2024-07-04 05:19:32 +00:00
self.client
.perform_trade_action(TradeAction::Accept(trade.phase));
2024-07-07 08:52:25 +00:00
// If they are offering more
2024-07-13 05:39:18 +00:00
} else if difference > 0 {
2024-07-07 08:52:25 +00:00
// If they are offering coins
if their_offered_coin_amount > 0 {
if let Some(their_coins) = get_their_coins {
// Remove their coins to balance
self.client.perform_trade_action(TradeAction::RemoveItem {
item: their_coins,
quantity: difference as u32,
ours: false,
});
}
2024-07-07 08:52:25 +00:00
// If they are not offering coins
} else if let Some(my_coins) = get_my_coins {
2024-07-07 08:52:25 +00:00
// 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
2024-07-13 05:39:18 +00:00
} else if difference < 0 {
2024-07-07 08:52:25 +00:00
// If I am offering coins
if my_offered_coin_amount > 0 {
if let Some(my_coins) = get_my_coins {
// Remove my coins to balance
self.client.perform_trade_action(TradeAction::RemoveItem {
item: my_coins,
quantity: difference.unsigned_abs(),
ours: true,
});
}
2024-07-07 08:52:25 +00:00
// If I am not offering coins
} else if let Some(their_coins) = get_their_coins {
2024-07-07 08:52:25 +00:00
// Add their coins to balance
self.client.perform_trade_action(TradeAction::AddItem {
item: their_coins,
quantity: difference.unsigned_abs(),
2024-07-07 08:52:25 +00:00
ours: false,
});
}
2024-07-03 23:04:49 +00:00
}
Ok(())
}
2024-07-13 04:37:16 +00:00
/// Attempts to find an item based on a search term and sends the price info to the target
/// player.
2024-07-16 11:51:19 +00:00
///
/// The search is case-insensitive. It searches both the item name then, if the search term is
/// not found, it searches the item's ID as written in the configuration file.
2024-07-13 04:37:16 +00:00
fn send_price_info(&mut self, target: &Uid, search_term: &str) -> Result<(), String> {
2024-07-16 11:51:19 +00:00
let search_term = search_term.to_lowercase();
2024-07-07 08:52:25 +00:00
let player_name = self
2024-07-16 09:37:07 +00:00
.find_player_alias(target)
2024-07-07 08:52:25 +00:00
.ok_or("Failed to find player name")?
.to_string();
let mut found = false;
2024-07-07 08:52:25 +00:00
for (item_id, price) in &self.buy_prices {
2024-07-16 11:51:19 +00:00
let item_name = self.get_item_name(item_id).to_lowercase();
2024-07-16 09:31:24 +00:00
2024-07-16 11:51:19 +00:00
if item_name.contains(&search_term) {
2024-07-16 09:31:24 +00:00
log::info!("Sending price info on {item_name} to {player_name}");
self.client.send_command(
"tell".to_string(),
vec![
player_name.clone(),
format!("Selling {item_name} for {price} coins."),
],
);
found = true;
continue;
}
2024-07-16 11:51:19 +00:00
if item_id.contains(&search_term) {
let short_id = item_id.splitn(3, '.').last().unwrap_or_default();
2024-07-13 04:39:10 +00:00
log::info!("Sending price info on {short_id} to {player_name}");
self.client.send_command(
"tell".to_string(),
vec![
player_name.clone(),
format!("Buying {short_id} for {price} coins."),
],
);
found = true;
}
}
for (item_id, price) in &self.sell_prices {
2024-07-16 11:51:19 +00:00
let item_name = self.get_item_name(item_id).to_lowercase();
2024-07-16 09:31:24 +00:00
2024-07-16 11:51:19 +00:00
if item_name.contains(&search_term) {
2024-07-16 09:31:24 +00:00
log::info!("Sending price info on {item_name} to {player_name}");
self.client.send_command(
"tell".to_string(),
vec![
player_name.clone(),
format!("Selling {item_name} for {price} coins."),
],
);
found = true;
continue;
}
2024-07-16 11:51:19 +00:00
if item_id.contains(&search_term) {
let short_id = item_id.splitn(3, '.').last().unwrap_or_default();
2024-07-13 04:39:10 +00:00
log::info!("Sending price info on {short_id} to {player_name}");
self.client.send_command(
"tell".to_string(),
vec![
player_name.clone(),
format!("Selling {short_id} for {price} coins."),
],
);
found = true;
}
}
if !found {
2024-07-13 04:39:10 +00:00
log::info!("Found no price for \"{search_term}\" for {player_name}");
self.client.send_command(
"tell".to_string(),
vec![player_name, format!("I don't have a price for that item.")],
);
}
2024-07-07 08:52:25 +00:00
Ok(())
}
2024-07-16 09:37:07 +00:00
/// Determines if the Uid belongs to an admin.
fn is_user_admin(&self, uid: &Uid) -> Result<bool, String> {
let sender_uuid = self
.find_uuid(uid)
.ok_or("Failed to find uuid")?
.to_string();
2024-07-16 09:37:07 +00:00
let sender_name = self.find_player_alias(uid).ok_or("Failed to find name")?;
Ok(self.admins.contains(sender_name) || self.admins.contains(&sender_uuid))
}
2024-07-13 04:37:16 +00:00
/// Moves the character to the configured position and orientation.
2024-07-08 14:39:19 +00:00
fn handle_position_and_orientation(&mut self) -> Result<(), String> {
2024-07-12 15:59:28 +00:00
if let Some(current_position) = self.client.current::<Pos>() {
let target_position = Pos(self.position.into());
if current_position != target_position {
let entity = self.client.entity();
let ecs = self.client.state_mut().ecs();
let mut position_state = ecs.write_storage::<Pos>();
2024-07-08 23:06:41 +00:00
2024-07-12 15:59:28 +00:00
position_state
.insert(entity, target_position)
.map_err(|error| error.to_string())?;
2024-07-08 17:59:35 +00:00
}
2024-07-08 14:39:19 +00:00
}
2024-07-12 15:59:28 +00:00
if let Some(current_orientation) = self.client.current::<Ori>() {
let target_orientation = Ori::default()
.uprighted()
.rotated(Quaternion::rotation_z(self.orientation.to_radians()));
if current_orientation != target_orientation {
let entity = self.client.entity();
let ecs = self.client.state_mut().ecs();
let mut orientation_state = ecs.write_storage::<Ori>();
orientation_state
.insert(entity, target_orientation)
.map_err(|error| error.to_string())?;
}
}
2024-07-08 14:39:19 +00:00
Ok(())
}
2024-07-16 11:51:19 +00:00
/// Gets the name of an item from its id.
fn get_item_name(&self, item_id: &str) -> String {
2024-07-16 09:37:07 +00:00
let item = Item::new_from_item_definition_id(
2024-07-16 11:51:19 +00:00
ItemDefinitionId::Simple(Cow::Borrowed(item_id)),
2024-07-16 09:37:07 +00:00
&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)
}
2024-07-13 04:37:16 +00:00
/// Finds the name of a player by their Uid.
2024-07-16 09:37:07 +00:00
fn find_player_alias<'a>(&'a self, uid: &Uid) -> Option<&'a String> {
2024-07-11 17:18:55 +00:00
self.client.player_list().iter().find_map(|(id, info)| {
if id == uid {
return Some(&info.player_alias);
}
None
})
}
2024-07-13 04:37:16 +00:00
/// Finds the Uuid of a player by their Uid.
fn find_uuid(&self, target: &Uid) -> Option<Uuid> {
self.client.player_list().iter().find_map(|(uid, info)| {
if uid == target {
Some(info.uuid)
} else {
None
}
})
}
2024-07-13 04:37:16 +00:00
/// Finds the Uid of a player by their name.
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)
} else {
None
}
})
}
2024-07-03 23:04:49 +00:00
}
2024-07-13 05:39:18 +00:00
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2024-07-08 19:15:33 +00:00
enum TradeMode {
AdminAccess,
2024-07-08 19:15:33 +00:00
Trade,
}
2024-07-03 23:04:49 +00:00
fn connect_to_veloren(username: &str, password: &str) -> Result<Client, String> {
let runtime = Arc::new(Runtime::new().unwrap());
let runtime2 = Arc::clone(&runtime);
runtime
.block_on(Client::new(
ConnectionArgs::Tcp {
hostname: "server.veloren.net".to_string(),
prefer_ipv6: false,
},
runtime2,
&mut None,
username,
password,
None,
|provider| provider == "https://auth.veloren.net",
&|_| {},
|_| {},
Default::default(),
))
.map_err(|error| format!("{error:?}"))
}