Compare commits
2 Commits
637ead2914
...
0f6edb1d0c
Author | SHA1 | Date | |
---|---|---|---|
0f6edb1d0c | |||
cf05f33d7a |
@ -78,7 +78,7 @@ Then follow the [above](#running) steps with the tag "trade_bot" instead of "git
|
|||||||
The bot is able to respond to the following commands, which must be sent via "/tell".
|
The bot is able to respond to the following commands, which must be sent via "/tell".
|
||||||
|
|
||||||
- `price [search term]`: Returns the buy/sell offers of any item whose item definition ID contains the search term.
|
- `price [search term]`: Returns the buy/sell offers of any item whose item definition ID contains the search term.
|
||||||
- `admin_access`: Admin-only, prompts the bot to send a trade invite, after which it will give away and accept any items until the trade ends.
|
- `admin_access`: Admin-only, prompts the bot to send a trade invite to the sender, after which it will give away and accept any items until the trade ends.
|
||||||
- `sort [count (optional)]`: Admin-only, sorts the inventory once or the given number of times.
|
- `sort [count (optional)]`: Admin-only, sorts the inventory once or the given number of times.
|
||||||
- `position [x] [y] [z]`: Admin-only, sets the bot's desired position where it will try to stand (must be close to the character)
|
- `position [x] [y] [z]`: Admin-only, sets the bot's desired position where it will try to stand (must be close to the character)
|
||||||
- `orientation [0-360]`: Admin-only, sets the bot's desired orientation (or facing direction)
|
- `orientation [0-360]`: Admin-only, sets the bot's desired orientation (or facing direction)
|
||||||
|
89
src/bot.rs
89
src/bot.rs
@ -1,3 +1,12 @@
|
|||||||
|
/**
|
||||||
|
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.
|
||||||
|
**/
|
||||||
use std::{
|
use std::{
|
||||||
collections::HashMap,
|
collections::HashMap,
|
||||||
sync::Arc,
|
sync::Arc,
|
||||||
@ -21,8 +30,10 @@ use veloren_common_net::sync::WorldSyncExt;
|
|||||||
|
|
||||||
const COINS: &str = "common.items.utility.coins";
|
const COINS: &str = "common.items.utility.coins";
|
||||||
|
|
||||||
/// A Bot instance represents an active connection to the server and it will
|
/// An active connection to the Veloren server that will attempt to run every time the `tick`
|
||||||
/// attempt to run every time the `tick` function is called.
|
/// function is called.
|
||||||
|
///
|
||||||
|
/// See the [module-level documentation](index.html) for more information.
|
||||||
pub struct Bot {
|
pub struct Bot {
|
||||||
username: String,
|
username: String,
|
||||||
position: [f32; 3],
|
position: [f32; 3],
|
||||||
@ -112,7 +123,10 @@ impl Bot {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Run the bot for a single tick. This should be called in a loop.
|
/// Run the bot for a single tick. This should be called in a loop.
|
||||||
|
///
|
||||||
|
/// 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.
|
||||||
pub fn tick(&mut self) -> Result<(), String> {
|
pub fn tick(&mut self) -> Result<(), String> {
|
||||||
let veloren_events = self
|
let veloren_events = self
|
||||||
.client
|
.client
|
||||||
@ -193,11 +207,11 @@ impl Bot {
|
|||||||
.parse::<u8>()
|
.parse::<u8>()
|
||||||
.map_err(|error| error.to_string())?;
|
.map_err(|error| error.to_string())?;
|
||||||
|
|
||||||
log::debug!("Sorting inventory {sort_count} times");
|
log::info!("Sorting inventory {sort_count} times");
|
||||||
|
|
||||||
self.sort_count = sort_count;
|
self.sort_count = sort_count;
|
||||||
} else {
|
} else {
|
||||||
log::debug!("Sorting inventory");
|
log::info!("Sorting inventory");
|
||||||
self.client.sort_inventory();
|
self.client.sort_inventory();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -208,7 +222,7 @@ impl Bot {
|
|||||||
}
|
}
|
||||||
"admin_access" => {
|
"admin_access" => {
|
||||||
if self.is_user_admin(&sender)? && !self.client.is_trading() {
|
if self.is_user_admin(&sender)? && !self.client.is_trading() {
|
||||||
log::debug!("Providing admin access");
|
log::info!("Providing admin access");
|
||||||
|
|
||||||
self.trade_mode = TradeMode::AdminAccess;
|
self.trade_mode = TradeMode::AdminAccess;
|
||||||
self.client.send_invite(sender, InviteKind::Trade);
|
self.client.send_invite(sender, InviteKind::Trade);
|
||||||
@ -309,18 +323,28 @@ impl Bot {
|
|||||||
let their_uid = trade.parties[their_party];
|
let their_uid = trade.parties[their_party];
|
||||||
let their_name = self.find_name(&their_uid).ok_or("Failed to find name")?;
|
let their_name = self.find_name(&their_uid).ok_or("Failed to find name")?;
|
||||||
|
|
||||||
log::info!("End of trade with {their_name}: {result:?}");
|
match result {
|
||||||
|
TradeResult::Completed => {
|
||||||
|
log::info!(
|
||||||
|
"Trade with {their_name}: {:?} {:?}",
|
||||||
|
trade.offers[0],
|
||||||
|
trade.offers[1]
|
||||||
|
);
|
||||||
|
|
||||||
if let TradeMode::AdminAccess = self.trade_mode {
|
|
||||||
self.trade_mode = TradeMode::Trade;
|
|
||||||
}
|
|
||||||
|
|
||||||
if let TradeResult::Completed = result {
|
|
||||||
self.client.send_command(
|
self.client.send_command(
|
||||||
"say".to_string(),
|
"say".to_string(),
|
||||||
vec!["Thank you for trading with me!".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")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let TradeMode::AdminAccess = self.trade_mode {
|
||||||
|
self.trade_mode = TradeMode::Trade;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
_ => (),
|
_ => (),
|
||||||
}
|
}
|
||||||
@ -328,6 +352,10 @@ impl Bot {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Make the bot's trading and help accouncements
|
||||||
|
//
|
||||||
|
// Currently, this will make two announcements: one in /region with basic usage instructions
|
||||||
|
// and one in /world with the [Bot::announcment] field followed by " at [location]."
|
||||||
fn handle_announcement(&mut self) -> Result<(), String> {
|
fn handle_announcement(&mut self) -> Result<(), String> {
|
||||||
log::info!("Making an announcement");
|
log::info!("Making an announcement");
|
||||||
|
|
||||||
@ -362,6 +390,7 @@ impl Bot {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Use the lantern at night and put it away during the day.
|
||||||
fn handle_lantern(&mut self) {
|
fn handle_lantern(&mut self) {
|
||||||
let day_period = self.client.state().get_day_period();
|
let day_period = self.client.state().get_day_period();
|
||||||
|
|
||||||
@ -379,6 +408,23 @@ impl Bot {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 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 total value of each side of
|
||||||
|
/// the trade. Coins are hard-coded to have a value of 1 each.
|
||||||
|
///
|
||||||
|
/// The bot's trading logic is as follows:
|
||||||
|
/// 1. If the trade is empty, do nothing.
|
||||||
|
/// 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 some to balance.
|
||||||
|
/// 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 some to balance.
|
||||||
|
/// 2. If I am not offering coins, add theirs to balance.
|
||||||
fn handle_trade(&mut self, trade: PendingTrade) -> Result<(), String> {
|
fn handle_trade(&mut self, trade: PendingTrade) -> Result<(), String> {
|
||||||
if trade.is_empty_trade() {
|
if trade.is_empty_trade() {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
@ -580,7 +626,9 @@ impl Bot {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn send_price_info(&mut self, target: &Uid, item_name: &str) -> Result<(), String> {
|
/// Attempts to find an item based on a search term and sends the price info to the target
|
||||||
|
/// player.
|
||||||
|
fn send_price_info(&mut self, target: &Uid, search_term: &str) -> Result<(), String> {
|
||||||
let player_name = self
|
let player_name = self
|
||||||
.find_name(target)
|
.find_name(target)
|
||||||
.ok_or("Failed to find player name")?
|
.ok_or("Failed to find player name")?
|
||||||
@ -588,10 +636,10 @@ impl Bot {
|
|||||||
let mut found = false;
|
let mut found = false;
|
||||||
|
|
||||||
for (item_id, price) in &self.buy_prices {
|
for (item_id, price) in &self.buy_prices {
|
||||||
if item_id.contains(item_name) {
|
if item_id.contains(search_term) {
|
||||||
let short_id = item_id.splitn(3, '.').last().unwrap_or_default();
|
let short_id = item_id.splitn(3, '.').last().unwrap_or_default();
|
||||||
|
|
||||||
log::debug!("Sending price info on {short_id} to {player_name}");
|
log::info!("Sending price info on {short_id} to {player_name}");
|
||||||
|
|
||||||
self.client.send_command(
|
self.client.send_command(
|
||||||
"tell".to_string(),
|
"tell".to_string(),
|
||||||
@ -606,10 +654,10 @@ impl Bot {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for (item_id, price) in &self.sell_prices {
|
for (item_id, price) in &self.sell_prices {
|
||||||
if item_id.contains(item_name) {
|
if item_id.contains(search_term) {
|
||||||
let short_id = item_id.splitn(3, '.').last().unwrap_or_default();
|
let short_id = item_id.splitn(3, '.').last().unwrap_or_default();
|
||||||
|
|
||||||
log::debug!("Sending price info on {short_id} to {player_name}");
|
log::info!("Sending price info on {short_id} to {player_name}");
|
||||||
|
|
||||||
self.client.send_command(
|
self.client.send_command(
|
||||||
"tell".to_string(),
|
"tell".to_string(),
|
||||||
@ -624,7 +672,7 @@ impl Bot {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if !found {
|
if !found {
|
||||||
log::debug!("Found no price for \"{item_name}\" for {player_name}");
|
log::info!("Found no price for \"{search_term}\" for {player_name}");
|
||||||
|
|
||||||
self.client.send_command(
|
self.client.send_command(
|
||||||
"tell".to_string(),
|
"tell".to_string(),
|
||||||
@ -635,6 +683,7 @@ impl Bot {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Determines if the Uid belonds to an admin.
|
||||||
fn is_user_admin(&self, uid: &Uid) -> Result<bool, String> {
|
fn is_user_admin(&self, uid: &Uid) -> Result<bool, String> {
|
||||||
let sender_uuid = self
|
let sender_uuid = self
|
||||||
.find_uuid(uid)
|
.find_uuid(uid)
|
||||||
@ -645,6 +694,7 @@ impl Bot {
|
|||||||
Ok(self.admins.contains(sender_name) || self.admins.contains(&sender_uuid))
|
Ok(self.admins.contains(sender_name) || self.admins.contains(&sender_uuid))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Moves the character to the configured position and orientation.
|
||||||
fn handle_position_and_orientation(&mut self) -> Result<(), String> {
|
fn handle_position_and_orientation(&mut self) -> Result<(), String> {
|
||||||
if let Some(current_position) = self.client.current::<Pos>() {
|
if let Some(current_position) = self.client.current::<Pos>() {
|
||||||
let target_position = Pos(self.position.into());
|
let target_position = Pos(self.position.into());
|
||||||
@ -679,6 +729,7 @@ impl Bot {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Finds the name of a player by their Uid.
|
||||||
fn find_name<'a>(&'a self, uid: &Uid) -> Option<&'a String> {
|
fn find_name<'a>(&'a self, uid: &Uid) -> Option<&'a String> {
|
||||||
self.client.player_list().iter().find_map(|(id, info)| {
|
self.client.player_list().iter().find_map(|(id, info)| {
|
||||||
if id == uid {
|
if id == uid {
|
||||||
@ -689,6 +740,7 @@ impl Bot {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Finds the Uuid of a player by their Uid.
|
||||||
fn find_uuid(&self, target: &Uid) -> Option<Uuid> {
|
fn find_uuid(&self, target: &Uid) -> Option<Uuid> {
|
||||||
self.client.player_list().iter().find_map(|(uid, info)| {
|
self.client.player_list().iter().find_map(|(uid, info)| {
|
||||||
if uid == target {
|
if uid == target {
|
||||||
@ -699,6 +751,7 @@ impl Bot {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Finds the Uid of a player by their name.
|
||||||
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)| {
|
self.client.player_list().iter().find_map(|(id, info)| {
|
||||||
if info.player_alias == name {
|
if info.player_alias == name {
|
||||||
|
Loading…
Reference in New Issue
Block a user