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-13 03:54:44 +00:00
collections ::HashMap ,
2024-07-07 08:52:25 +00:00
sync ::Arc ,
time ::{ Duration , Instant } ,
2024-07-03 23:04:49 +00:00
} ;
use tokio ::runtime ::Runtime ;
2024-07-11 04:36:37 +00:00
use vek ::Quaternion ;
2024-07-12 00:11:44 +00:00
use veloren_client ::{ addr ::ConnectionArgs , Client , Event as VelorenEvent , SiteInfoRich , WorldExt } ;
2024-07-03 23:04:49 +00:00
use veloren_common ::{
2024-07-13 08:37:31 +00:00
character ::Character ,
2024-07-03 23:04:49 +00:00
clock ::Clock ,
2024-07-13 08:37:31 +00:00
comp ::{
invite ::InviteKind , item ::ItemDefinitionIdOwned , CharacterState , ChatType ,
ControllerInputs , Health , 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 ,
2024-07-11 04:36:37 +00:00
uuid ::Uuid ,
2024-07-11 14:40:31 +00:00
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 ] ,
2024-07-11 04:36:37 +00:00
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-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-13 05:39:18 +00:00
previous_offer : Option < ( HashMap < String , u32 > , HashMap < String , u32 > ) > ,
2024-07-12 00:11:44 +00:00
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 {
2024-07-08 20:58:40 +00:00
/// Connect to the official veloren server, select the specified character
2024-07-11 14:40:31 +00:00
/// 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 ,
2024-07-08 20:58:40 +00:00
character : & str ,
2024-07-11 04:36:37 +00:00
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 ] ,
2024-07-11 04:36:37 +00:00
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 ) ? ;
2024-07-08 20:58:40 +00:00
let mut clock = Clock ::new ( Duration ::from_secs_f64 ( 1.0 / 30.0 ) ) ;
2024-07-03 23:04:49 +00:00
2024-07-07 08:52:25 +00:00
log ::info! ( " Selecting a character " ) ;
2024-07-08 20:58:40 +00:00
client . load_character_list ( ) ;
2024-07-03 23:04:49 +00:00
2024-07-08 20:58:40 +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:?} " ) ) ? ;
2024-07-08 20:58:40 +00:00
clock . tick ( ) ;
2024-07-03 23:04:49 +00:00
}
2024-07-08 20:58:40 +00:00
let character_id = client
2024-07-03 23:04:49 +00:00
. character_list ( )
. characters
2024-07-08 20:58:40 +00:00
. 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-13 08:37:31 +00:00
// This loop waits and retries requesting the character in the case that the character has
// logged out to 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
2024-07-08 20:58:40 +00:00
let now = Instant ::now ( ) ;
Ok ( Bot {
2024-07-11 06:58:52 +00:00
username ,
2024-07-08 20:58:40 +00:00
position ,
orientation ,
2024-07-11 04:36:37 +00:00
admins ,
2024-07-08 20:58:40 +00:00
client ,
clock ,
buy_prices ,
sell_prices ,
trade_mode : TradeMode ::Trade ,
2024-07-13 05:39:18 +00:00
previous_offer : None ,
2024-07-12 00:11:44 +00:00
last_trade_action : now ,
2024-07-08 20:58:40 +00:00
last_announcement : now ,
last_ouch : now ,
2024-07-11 18:30:50 +00:00
sort_count : 0 ,
2024-07-12 00:11:44 +00:00
announcement ,
2024-07-08 20:58:40 +00:00
} )
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
2024-07-13 07:51:37 +00:00
/// also accepts incoming trade invites, which has a potential for error if the bot accepts an /// invite while in the wrong trade mode. See the inline comments for more information.
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
}
2024-07-12 00:11:44 +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 {
2024-07-11 15:43:52 +00:00
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
}
2024-07-13 07:51:37 +00:00
// It should be enough to check the trade mode but the extra check for outgoing
// invites ensures that the bot doesn't accept an invite while in AdminAccess mode.
// This cannot happen because the bot doesn't send invites in Trade mode but the code
// is here to be extra safe for future refactoring.
} else if TradeMode ::Trade = = self . trade_mode
& & self . client . pending_invites ( ) . is_empty ( )
2024-07-13 05:39:18 +00:00
{
2024-07-07 08:52:25 +00:00
self . client . accept_invite ( ) ;
}
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 ) {
2024-07-12 00:11:44 +00:00
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-08 23:48:08 +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 {
2024-07-09 00:16:10 +00:00
" price " = > {
2024-07-11 03:57:25 +00:00
for item_name in split_content {
2024-07-11 15:43:52 +00:00
self . send_price_info ( & sender , & item_name . to_lowercase ( ) ) ? ;
}
2024-07-12 15:59:28 +00:00
None
2024-07-11 15:43:52 +00:00
}
" sort " = > {
if self . is_user_admin ( & sender ) ? {
if let Some ( sort_count ) = split_content . next ( ) {
let sort_count = sort_count
. parse ::< u8 > ( )
. map_err ( | error | error . to_string ( ) ) ? ;
2024-07-13 04:39:10 +00:00
log ::info! ( " Sorting inventory {sort_count} times " ) ;
2024-07-11 15:43:52 +00:00
2024-07-12 00:11:44 +00:00
self . sort_count = sort_count ;
2024-07-11 15:43:52 +00:00
} else {
self . client . sort_inventory ( ) ;
2024-07-13 07:51:37 +00:00
log ::info! ( " Sorting inventory once " ) ;
2024-07-11 15:43:52 +00:00
}
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
}
2024-07-11 15:43:52 +00:00
" admin_access " = > {
if self . is_user_admin ( & sender ) ? & & ! self . client . is_trading ( ) {
2024-07-13 04:39:10 +00:00
log ::info! ( " Providing admin access " ) ;
2024-07-11 15:43:52 +00:00
2024-07-13 05:39:18 +00:00
self . previous_offer = None ;
2024-07-11 15:43:52 +00:00
self . trade_mode = TradeMode ::AdminAccess ;
2024-07-13 05:39:18 +00:00
2024-07-09 00:16:10 +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-09 00:16:10 +00:00
}
}
2024-07-11 17:18:55 +00:00
" position " = > {
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 'position [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
}
}
2024-07-12 15:59:28 +00:00
" orientation " = > {
if self . is_user_admin ( & sender ) ? {
if let Some ( orientation ) = split_content . next ( ) {
self . orientation = orientation
. parse ::< f32 > ( )
. map_err ( | error | error . to_string ( ) ) ? ;
2024-07-11 03:57:25 +00:00
2024-07-12 15:59:28 +00:00
None
} else {
Some ( " Use the format 'orientation [0-360]' " )
}
} 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
. find_name ( & sender )
. 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 , .. } ) = > {
2024-07-11 14:40:31 +00:00
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 ( ) ;
}
}
}
2024-07-11 14:40:31 +00:00
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 ] ;
let their_name = self . find_name ( & their_uid ) . ok_or ( " Failed to find name " ) ? ;
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
2024-07-11 15:43:52 +00:00
if let TradeMode ::AdminAccess = self . trade_mode {
2024-07-13 05:39:18 +00:00
log ::info! ( " End of admin access " ) ;
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-13 04:37:16 +00:00
// Make the bot's trading and help accouncements
//
2024-07-13 07:51:37 +00:00
// 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 followed by " at [location]."
2024-07-12 00:11:44 +00:00
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 ] ) ;
}
2024-07-12 00:11:44 +00:00
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 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.
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 ( ( ) ) ;
}
2024-07-11 14:40:31 +00:00
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-07 08:52:25 +00:00
let ( my_offer , their_offer , them ) = {
2024-07-06 01:48:26 +00:00
(
& trade . offers [ my_offer_index ] ,
& trade . offers [ their_offer_index ] ,
2024-07-07 08:52:25 +00:00
self . client
. state ( )
. ecs ( )
. entity_from_uid ( trade . parties [ their_offer_index ] )
. ok_or ( " Failed to find player " . to_string ( ) ) ? ,
2024-07-06 01:48:26 +00:00
)
} ;
2024-07-04 05:19:32 +00:00
let inventories = self . client . inventories ( ) ;
2024-07-11 02:06:12 +00:00
let my_inventory = inventories
. get ( self . client . entity ( ) )
. ok_or ( " Failed to find inventory " ) ? ;
let get_my_coins = my_inventory
. get_slot_of_item_by_def_id ( & ItemDefinitionIdOwned ::Simple ( COINS . to_string ( ) ) ) ;
let their_inventory = inventories . get ( them ) . ok_or ( " Failed to find inventory " ) ? ;
let get_their_coins = their_inventory
. get_slot_of_item_by_def_id ( & ItemDefinitionIdOwned ::Simple ( COINS . to_string ( ) ) ) ;
2024-07-07 08:52:25 +00:00
let ( mut their_offered_coin_amount , mut my_offered_coin_amount ) = ( 0 , 0 ) ;
2024-07-04 05:19:32 +00:00
let their_offered_items_value =
2024-07-07 08:52:25 +00:00
their_offer
. into_iter ( )
. fold ( 0 , | acc : i32 , ( slot_id , quantity ) | {
2024-07-08 18:38:23 +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 {
their_offered_coin_amount = * quantity 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 )
} )
} ;
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 ) | {
2024-07-08 18:38:23 +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 {
my_offered_coin_amount = * quantity as i32 ;
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 )
2024-07-08 18:21:50 +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-03 23:04:49 +00:00
2024-07-04 18:40:07 +00:00
let mut my_items_to_remove = Vec ::new ( ) ;
2024-07-07 08:52:25 +00:00
for ( slot_id , amount ) in my_offer {
2024-07-08 18:38:23 +00:00
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 ) {
2024-07-08 18:38:23 +00:00
my_items_to_remove . push ( ( * slot_id , * amount ) ) ;
2024-07-04 18:40:07 +00:00
}
}
let mut their_items_to_remove = Vec ::new ( ) ;
2024-07-07 08:52:25 +00:00
for ( slot_id , amount ) in their_offer {
2024-07-08 18:38:23 +00:00
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 ) {
2024-07-08 18:38:23 +00:00
their_items_to_remove . push ( ( * slot_id , * amount ) ) ;
2024-07-04 18:40:07 +00:00
}
}
2024-07-13 05:39:18 +00:00
let item_offers = {
let mut their_items = HashMap ::with_capacity ( their_offer . len ( ) ) ;
2024-07-03 23:04:49 +00:00
2024-07-13 05:39:18 +00:00
for ( slot_id , quantity ) in their_offer {
if let Some ( item ) = their_inventory . get ( * slot_id ) {
their_items . insert ( item . persistence_item_id ( ) , * quantity ) ;
}
}
let mut my_items = HashMap ::with_capacity ( my_offer . len ( ) ) ;
for ( slot_id , quantity ) in my_offer {
if let Some ( item ) = my_inventory . get ( * slot_id ) {
my_items . insert ( item . persistence_item_id ( ) , * quantity ) ;
}
}
( my_items , their_items )
} ;
drop ( inventories ) ;
2024-07-03 23:04:49 +00:00
2024-07-07 08:52:25 +00:00
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 ,
} ) ;
2024-07-06 01:48:26 +00:00
}
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
}
2024-07-07 08:52:25 +00:00
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 ,
} ) ;
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
}
2024-07-08 18:38:23 +00:00
let difference = their_offered_items_value - my_offered_items_value ;
2024-07-04 18:40:07 +00:00
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-13 05:39:18 +00:00
self . previous_offer = Some ( item_offers ) ;
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 {
2024-07-11 14:40:31 +00:00
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
2024-07-11 14:40:31 +00:00
} 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 {
2024-07-11 14:40:31 +00:00
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
2024-07-11 14:40:31 +00:00
} 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 ,
2024-07-08 18:38:23 +00:00
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.
fn send_price_info ( & mut self , target : & Uid , search_term : & str ) -> Result < ( ) , String > {
2024-07-07 08:52:25 +00:00
let player_name = self
. find_name ( target )
. ok_or ( " Failed to find player name " ) ?
. to_string ( ) ;
2024-07-11 02:06:12 +00:00
let mut found = false ;
2024-07-07 08:52:25 +00:00
2024-07-11 02:06:12 +00:00
for ( item_id , price ) in & self . buy_prices {
2024-07-13 04:37:16 +00:00
if item_id . contains ( search_term ) {
2024-07-11 02:06:12 +00:00
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} " ) ;
2024-07-11 02:06:12 +00:00
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-13 04:37:16 +00:00
if item_id . contains ( search_term ) {
2024-07-11 02:06:12 +00:00
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} " ) ;
2024-07-11 02:06:12 +00:00
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} " ) ;
2024-07-11 02:06:12 +00:00
self . client . send_command (
" tell " . to_string ( ) ,
2024-07-11 04:36:37 +00:00
vec! [ player_name , format! ( " I don't have a price for that item. " ) ] ,
2024-07-11 02:06:12 +00:00
) ;
}
2024-07-07 08:52:25 +00:00
Ok ( ( ) )
}
2024-07-13 04:37:16 +00:00
/// Determines if the Uid belonds to an admin.
2024-07-11 15:43:52 +00:00
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 ( ) ;
let sender_name = self . find_name ( 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-11 02:06:12 +00:00
2024-07-13 04:37:16 +00:00
/// Finds the name of a player by their Uid.
2024-07-11 17:18:55 +00:00
fn find_name < ' a > ( & ' a self , uid : & Uid ) -> Option < & ' a String > {
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.
2024-07-11 04:36:37 +00:00
fn find_uuid ( & self , target : & Uid ) -> Option < Uuid > {
self . client . player_list ( ) . iter ( ) . find_map ( | ( uid , info ) | {
if uid = = target {
Some ( info . uuid )
2024-07-11 02:06:12 +00:00
} else {
None
}
} )
}
2024-07-13 04:37:16 +00:00
/// Finds the Uid of a player by their name.
2024-07-11 04:36:37 +00:00
fn _find_uid < ' a > ( & ' a self , name : & str ) -> Option < & ' a Uid > {
self . client . player_list ( ) . iter ( ) . find_map ( | ( id , info ) | {
2024-07-11 02:06:12 +00:00
if info . player_alias = = name {
2024-07-11 04:36:37 +00:00
Some ( id )
2024-07-11 02:06:12 +00:00
} 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 {
2024-07-11 15:43:52 +00:00
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:?} " ) )
}