2024-07-03 23:04:49 +00:00
|
|
|
use std::{
|
2024-07-04 05:19:32 +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-08 14:39:19 +00:00
|
|
|
use vek::{num_traits::Float, Quaternion};
|
2024-07-04 18:40:07 +00:00
|
|
|
use veloren_client::{addr::ConnectionArgs, Client, Event as VelorenEvent, WorldExt};
|
2024-07-03 23:04:49 +00:00
|
|
|
use veloren_common::{
|
|
|
|
clock::Clock,
|
2024-07-08 14:39:19 +00:00
|
|
|
comp::{invite::InviteKind, item::ItemDefinitionIdOwned, ChatType, ControllerInputs, 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,
|
|
|
|
ViewDistances,
|
|
|
|
};
|
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-08 20:58:40 +00:00
|
|
|
/// A Bot instance represents an active connection to the server and it will
|
|
|
|
/// attempt to run every time the `tick` function is called.
|
2024-07-03 23:04:49 +00:00
|
|
|
pub struct Bot {
|
2024-07-04 18:40:07 +00:00
|
|
|
position: [f32; 3],
|
2024-07-08 14:39:19 +00:00
|
|
|
orientation: 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-04 05:19:32 +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-04 05:19:32 +00:00
|
|
|
last_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-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
|
|
|
|
/// return a Bot instance ready to run.
|
2024-07-04 05:19:32 +00:00
|
|
|
pub fn new(
|
2024-07-08 18:38:23 +00:00
|
|
|
username: &str,
|
2024-07-04 05:19:32 +00:00
|
|
|
password: &str,
|
2024-07-08 20:58:40 +00:00
|
|
|
character: &str,
|
2024-07-04 05:19:32 +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-08 14:39:19 +00:00
|
|
|
orientation: 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-08 20:58:40 +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
|
|
|
|
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-09 02:54:42 +00:00
|
|
|
.expect(&format!("No character named {character}"))
|
2024-07-03 23:04:49 +00:00
|
|
|
.character
|
|
|
|
.id
|
|
|
|
.expect("Failed to get character ID");
|
|
|
|
|
2024-07-08 20:58:40 +00:00
|
|
|
client.request_character(
|
2024-07-03 23:04:49 +00:00
|
|
|
character_id,
|
|
|
|
ViewDistances {
|
2024-07-06 03:58:50 +00:00
|
|
|
terrain: 4,
|
|
|
|
entity: 4,
|
2024-07-03 23:04:49 +00:00
|
|
|
},
|
|
|
|
);
|
|
|
|
|
2024-07-08 20:58:40 +00:00
|
|
|
let now = Instant::now();
|
|
|
|
|
|
|
|
Ok(Bot {
|
|
|
|
position,
|
|
|
|
orientation,
|
|
|
|
client,
|
|
|
|
clock,
|
|
|
|
buy_prices,
|
|
|
|
sell_prices,
|
|
|
|
trade_mode: TradeMode::Trade,
|
|
|
|
last_action: now,
|
|
|
|
last_announcement: now,
|
|
|
|
last_ouch: now,
|
|
|
|
})
|
2024-07-03 23:04:49 +00:00
|
|
|
}
|
|
|
|
|
2024-07-08 23:48:08 +00:00
|
|
|
// Run the bot for a single tick. This should be called in a loop.
|
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-04 18:40:07 +00:00
|
|
|
if self.last_action.elapsed() > Duration::from_millis(300) {
|
|
|
|
if self.client.is_dead() {
|
|
|
|
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-07 08:52:25 +00:00
|
|
|
TradeMode::Trade => self.handle_trade(trade.clone())?,
|
2024-07-08 14:39:19 +00:00
|
|
|
TradeMode::Take => self.handle_take(trade.clone())?,
|
2024-07-04 05:19:32 +00:00
|
|
|
}
|
2024-07-08 14:39:19 +00:00
|
|
|
} else if self.client.pending_invites().is_empty() {
|
2024-07-07 08:52:25 +00:00
|
|
|
self.client.accept_invite();
|
|
|
|
}
|
|
|
|
|
2024-07-04 18:40:07 +00:00
|
|
|
self.last_action = Instant::now();
|
2024-07-03 23:04:49 +00:00
|
|
|
|
2024-07-08 19:15:33 +00:00
|
|
|
if self.last_announcement.elapsed() > Duration::from_secs(1200) {
|
|
|
|
self.client.send_command(
|
|
|
|
"region".to_string(),
|
2024-07-11 02:06:12 +00:00
|
|
|
vec!["I'm a bot. You can trade with me or use /say or /tell to check prices: 'price [item_name]'".to_string()],
|
2024-07-08 19:15:33 +00:00
|
|
|
);
|
2024-07-06 01:48:26 +00:00
|
|
|
|
2024-07-08 19:15:33 +00:00
|
|
|
self.last_announcement = 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-09 00:16:10 +00:00
|
|
|
let sender = match message.chat_type {
|
|
|
|
ChatType::Tell(uid, _) => uid,
|
|
|
|
ChatType::Say(uid) => uid,
|
|
|
|
_ => return Ok(()),
|
|
|
|
};
|
2024-07-04 18:40:07 +00:00
|
|
|
let content = message.content().as_plain().unwrap_or_default();
|
2024-07-09 00:16:10 +00:00
|
|
|
let (command, item_name) = content.split_once(' ').unwrap_or((content, ""));
|
2024-07-11 02:06:12 +00:00
|
|
|
let item_name = item_name.to_lowercase();
|
2024-07-09 00:16:10 +00:00
|
|
|
|
|
|
|
match command {
|
|
|
|
"price" => {
|
2024-07-11 02:06:12 +00:00
|
|
|
self.send_price_info(&sender, &item_name)?;
|
2024-07-03 23:04:49 +00:00
|
|
|
}
|
2024-07-09 00:16:10 +00:00
|
|
|
"take" => {
|
|
|
|
if !self.client.is_trading() {
|
|
|
|
self.trade_mode = TradeMode::Take;
|
|
|
|
self.client.send_invite(sender, InviteKind::Trade);
|
|
|
|
}
|
|
|
|
}
|
2024-07-04 18:40:07 +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-08 23:06:41 +00:00
|
|
|
if uid == target && self.last_ouch.elapsed() > Duration::from_secs(1) {
|
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(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-08 14:39:19 +00:00
|
|
|
VelorenEvent::TradeComplete { result, .. } => {
|
|
|
|
log::info!("Completed trade: {result:?}");
|
|
|
|
|
|
|
|
if let TradeMode::Take = self.trade_mode {
|
2024-07-09 00:16:10 +00:00
|
|
|
self.trade_mode = TradeMode::Trade;
|
2024-07-08 14:39:19 +00:00
|
|
|
}
|
2024-07-08 20:39:26 +00:00
|
|
|
|
|
|
|
if let TradeResult::Completed = result {
|
|
|
|
self.client.send_command(
|
|
|
|
"say".to_string(),
|
|
|
|
vec!["Thank you for trading with me!".to_string()],
|
|
|
|
);
|
|
|
|
}
|
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-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-07 08:52:25 +00:00
|
|
|
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 };
|
2024-07-09 00:16:10 +00:00
|
|
|
|
|
|
|
if trade.is_empty_trade() {
|
|
|
|
return Ok(());
|
|
|
|
}
|
|
|
|
|
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-04 05:19:32 +00:00
|
|
|
drop(inventories);
|
2024-07-03 23:04:49 +00:00
|
|
|
|
2024-07-07 08:52:25 +00:00
|
|
|
if their_offered_items_value == 0 && my_offered_items_value == 0 {
|
|
|
|
return Ok(());
|
2024-07-04 05:19:32 +00:00
|
|
|
}
|
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-11 02:06:12 +00:00
|
|
|
let (my_coins, their_coins) =
|
|
|
|
if let (Some(mine), Some(theirs)) = (get_my_coins, get_their_coins) {
|
|
|
|
(mine, theirs)
|
|
|
|
} else {
|
|
|
|
return Ok(());
|
|
|
|
};
|
|
|
|
|
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-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-04 05:19:32 +00:00
|
|
|
} else if difference.is_positive() {
|
2024-07-07 08:52:25 +00:00
|
|
|
// 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
|
2024-07-04 05:19:32 +00:00
|
|
|
} else if difference.is_negative() {
|
2024-07-07 08:52:25 +00:00
|
|
|
// 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,
|
2024-07-08 18:38:23 +00:00
|
|
|
quantity: difference.unsigned_abs(),
|
2024-07-07 08:52:25 +00:00
|
|
|
ours: true,
|
|
|
|
});
|
|
|
|
// If I am not offering coins
|
|
|
|
} else {
|
|
|
|
// 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-08 14:39:19 +00:00
|
|
|
fn handle_take(&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) = {
|
|
|
|
(
|
|
|
|
&trade.offers[my_offer_index],
|
|
|
|
&trade.offers[their_offer_index],
|
|
|
|
)
|
|
|
|
};
|
|
|
|
|
|
|
|
if my_offer.is_empty() && !their_offer.is_empty() {
|
2024-07-04 05:19:32 +00:00
|
|
|
self.client
|
|
|
|
.perform_trade_action(TradeAction::Accept(trade.phase));
|
|
|
|
}
|
2024-07-08 14:39:19 +00:00
|
|
|
|
|
|
|
Ok(())
|
2024-07-04 05:19:32 +00:00
|
|
|
}
|
|
|
|
|
2024-07-11 02:06:12 +00:00
|
|
|
fn send_price_info(&mut self, target: &Uid, item_name: &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 {
|
|
|
|
if item_id.contains(item_name) {
|
|
|
|
let short_id = item_id.splitn(3, '.').last().unwrap_or_default();
|
|
|
|
|
|
|
|
log::debug!("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 {
|
|
|
|
if item_id.contains(item_name) {
|
|
|
|
let short_id = item_id.splitn(3, '.').last().unwrap_or_default();
|
|
|
|
|
|
|
|
log::debug!("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 {
|
|
|
|
log::debug!("Found no price for \"{item_name}\" for {player_name}");
|
|
|
|
|
|
|
|
self.client.send_command(
|
|
|
|
"tell".to_string(),
|
|
|
|
vec![
|
|
|
|
player_name.clone(),
|
|
|
|
format!("I don't have a price for that item."),
|
|
|
|
],
|
|
|
|
);
|
|
|
|
}
|
2024-07-07 08:52:25 +00:00
|
|
|
|
|
|
|
Ok(())
|
|
|
|
}
|
|
|
|
|
2024-07-03 23:04:49 +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 {
|
2024-07-11 02:06:12 +00:00
|
|
|
return Some(&info.player_alias);
|
2024-07-03 23:04:49 +00:00
|
|
|
}
|
|
|
|
|
2024-07-11 02:06:12 +00:00
|
|
|
None
|
2024-07-03 23:04:49 +00:00
|
|
|
})
|
|
|
|
}
|
2024-07-08 14:39:19 +00:00
|
|
|
|
|
|
|
fn handle_position_and_orientation(&mut self) -> Result<(), String> {
|
2024-07-08 23:06:41 +00:00
|
|
|
let current_position = self.client.current::<Pos>();
|
|
|
|
|
|
|
|
if let Some(current_position) = current_position {
|
|
|
|
if current_position.0 == self.position.into() {
|
|
|
|
return Ok(());
|
2024-07-08 17:59:35 +00:00
|
|
|
}
|
2024-07-08 14:39:19 +00:00
|
|
|
}
|
|
|
|
|
2024-07-08 18:38:23 +00:00
|
|
|
let entity = self.client.entity();
|
2024-07-08 14:39:19 +00:00
|
|
|
let ecs = self.client.state_mut().ecs();
|
|
|
|
let mut position_state = ecs.write_storage::<Pos>();
|
|
|
|
let mut orientation_state = ecs.write_storage::<Ori>();
|
|
|
|
let orientation = match self.orientation.to_lowercase().as_str() {
|
|
|
|
"west" => Ori::default()
|
|
|
|
.uprighted()
|
|
|
|
.rotated(Quaternion::rotation_z(90.0.to_radians())),
|
|
|
|
"south" => Ori::default()
|
|
|
|
.uprighted()
|
|
|
|
.rotated(Quaternion::rotation_z(180.0.to_radians())),
|
|
|
|
"east" => Ori::default()
|
|
|
|
.uprighted()
|
|
|
|
.rotated(Quaternion::rotation_z(270.0.to_radians())),
|
|
|
|
"north" => Ori::default(),
|
|
|
|
_ => {
|
|
|
|
return Err("Orientation must north, east, south or west".to_string());
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
orientation_state
|
|
|
|
.insert(entity, orientation)
|
|
|
|
.map_err(|error| error.to_string())?;
|
|
|
|
position_state
|
|
|
|
.insert(entity, Pos(self.position.into()))
|
|
|
|
.map_err(|error| error.to_string())?;
|
|
|
|
|
|
|
|
Ok(())
|
|
|
|
}
|
2024-07-11 02:06:12 +00:00
|
|
|
|
|
|
|
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
|
|
|
|
}
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
fn _find_uuid(&self, name: &str) -> Option<String> {
|
|
|
|
self.client.player_list().iter().find_map(|(_, info)| {
|
|
|
|
if info.player_alias == name {
|
|
|
|
Some(info.uuid.to_string())
|
|
|
|
} else {
|
|
|
|
None
|
|
|
|
}
|
|
|
|
})
|
|
|
|
}
|
2024-07-03 23:04:49 +00:00
|
|
|
}
|
|
|
|
|
2024-07-08 19:15:33 +00:00
|
|
|
enum TradeMode {
|
|
|
|
Take,
|
|
|
|
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:?}"))
|
|
|
|
}
|