Optimize, add features and docs

Item removal during trade was simplified for
performance. The "announce" command was added to
force the bot to make announcements.
This commit is contained in:
Jeff 2024-07-15 22:18:41 -04:00
parent 31cfa10e0f
commit 91cbe84f4d
4 changed files with 249 additions and 68 deletions

146
Cargo.lock generated
View File

@ -590,6 +590,12 @@ version = "2.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e8566979429cf69b49a5c740c60791108e86440e8be149bbea4fe54d2c32d6e2" checksum = "e8566979429cf69b49a5c740c60791108e86440e8be149bbea4fe54d2c32d6e2"
[[package]]
name = "deunicode"
version = "1.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "339544cc9e2c4dc3fc7149fd630c5f22263a4fdf18a98afd0075784968b5cf00"
[[package]] [[package]]
name = "digest" name = "digest"
version = "0.10.7" version = "0.10.7"
@ -621,6 +627,17 @@ dependencies = [
"winapi", "winapi",
] ]
[[package]]
name = "displaydoc"
version = "0.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.66",
]
[[package]] [[package]]
name = "divrem" name = "divrem"
version = "1.0.0" version = "1.0.0"
@ -793,6 +810,47 @@ dependencies = [
"miniz_oxide", "miniz_oxide",
] ]
[[package]]
name = "fluent"
version = "0.16.0"
source = "git+https://github.com/juliancoffee/fluent-rs.git?branch=patched#929cf9512de121cce9b4cbf1cb860cd3294a1cd9"
dependencies = [
"fluent-bundle",
"unic-langid",
]
[[package]]
name = "fluent-bundle"
version = "0.15.2"
source = "git+https://github.com/juliancoffee/fluent-rs.git?branch=patched#929cf9512de121cce9b4cbf1cb860cd3294a1cd9"
dependencies = [
"fluent-langneg",
"fluent-syntax",
"intl-memoizer",
"intl_pluralrules",
"rustc-hash",
"self_cell 0.10.3",
"smallvec",
"unic-langid",
]
[[package]]
name = "fluent-langneg"
version = "0.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2c4ad0989667548f06ccd0e306ed56b61bd4d35458d54df5ec7587c0e8ed5e94"
dependencies = [
"unic-langid",
]
[[package]]
name = "fluent-syntax"
version = "0.11.0"
source = "git+https://github.com/juliancoffee/fluent-rs.git?branch=patched#929cf9512de121cce9b4cbf1cb860cd3294a1cd9"
dependencies = [
"thiserror",
]
[[package]] [[package]]
name = "fnv" name = "fnv"
version = "1.0.7" version = "1.0.7"
@ -1255,6 +1313,24 @@ version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0175f63815ce00183bf755155ad0cb48c65226c5d17a724e369c25418d2b7699" checksum = "0175f63815ce00183bf755155ad0cb48c65226c5d17a724e369c25418d2b7699"
[[package]]
name = "intl-memoizer"
version = "0.5.1"
source = "git+https://github.com/juliancoffee/fluent-rs.git?branch=patched#929cf9512de121cce9b4cbf1cb860cd3294a1cd9"
dependencies = [
"type-map",
"unic-langid",
]
[[package]]
name = "intl_pluralrules"
version = "7.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "078ea7b7c29a2b4df841a7f6ac8775ff6074020c6776d48491ce2268e068f972"
dependencies = [
"unic-langid",
]
[[package]] [[package]]
name = "ipconfig" name = "ipconfig"
version = "0.3.2" version = "0.3.2"
@ -2225,6 +2301,21 @@ dependencies = [
"libc", "libc",
] ]
[[package]]
name = "self_cell"
version = "0.10.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e14e4d63b804dc0c7ec4a1e52bcb63f02c7ac94476755aa579edac21e01f915d"
dependencies = [
"self_cell 1.0.4",
]
[[package]]
name = "self_cell"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d369a96f978623eb3dc28807c4852d6cc617fed53da5d3c400feff1ef34a714a"
[[package]] [[package]]
name = "semver" name = "semver"
version = "1.0.23" version = "1.0.23"
@ -2537,6 +2628,15 @@ dependencies = [
"slab", "slab",
] ]
[[package]]
name = "tinystr"
version = "0.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9117f5d4db391c1cf6927e7bea3db74b9a1c1add8f7eda9ffd5364f40f57b82f"
dependencies = [
"displaydoc",
]
[[package]] [[package]]
name = "tinyvec" name = "tinyvec"
version = "1.6.0" version = "1.6.0"
@ -2732,6 +2832,7 @@ dependencies = [
"toml", "toml",
"vek", "vek",
"veloren-client", "veloren-client",
"veloren-client-i18n",
"veloren-common", "veloren-common",
"veloren-common-net", "veloren-common-net",
"veloren-world", "veloren-world",
@ -2768,6 +2869,15 @@ dependencies = [
"nom", "nom",
] ]
[[package]]
name = "type-map"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "deb68604048ff8fa93347f02441e4487594adc20bb8a084f9e564d2b827a0a9f"
dependencies = [
"rustc-hash",
]
[[package]] [[package]]
name = "typenum" name = "typenum"
version = "1.17.0" version = "1.17.0"
@ -2780,6 +2890,24 @@ version = "0.10.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f720def6ce1ee2fc44d40ac9ed6d3a59c361c80a75a7aa8e75bb9baed31cf2ea" checksum = "f720def6ce1ee2fc44d40ac9ed6d3a59c361c80a75a7aa8e75bb9baed31cf2ea"
[[package]]
name = "unic-langid"
version = "0.9.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "23dd9d1e72a73b25e07123a80776aae3e7b0ec461ef94f9151eed6ec88005a44"
dependencies = [
"unic-langid-impl",
]
[[package]]
name = "unic-langid-impl"
version = "0.9.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0a5422c1f65949306c99240b81de9f3f15929f5a8bfe05bb44b034cc8bf593e5"
dependencies = [
"tinystr",
]
[[package]] [[package]]
name = "unicode-bidi" name = "unicode-bidi"
version = "0.3.15" version = "0.3.15"
@ -2879,6 +3007,24 @@ dependencies = [
"veloren-network", "veloren-network",
] ]
[[package]]
name = "veloren-client-i18n"
version = "0.13.0"
source = "git+https://gitlab.com/veloren/veloren?branch=master#9452500f169316264f2f71096531e9d9b5e87e19"
dependencies = [
"deunicode",
"fluent",
"fluent-bundle",
"fluent-syntax",
"hashbrown 0.14.5",
"intl-memoizer",
"serde",
"tracing",
"unic-langid",
"veloren-common-assets",
"veloren-common-i18n",
]
[[package]] [[package]]
name = "veloren-common" name = "veloren-common"
version = "0.10.0" version = "0.10.0"

View File

@ -11,6 +11,7 @@ veloren-common = { git = "https://gitlab.com/veloren/veloren", branch = "master"
veloren-common-net = { git = "https://gitlab.com/veloren/veloren", branch = "master" } veloren-common-net = { git = "https://gitlab.com/veloren/veloren", branch = "master" }
veloren-client = { git = "https://gitlab.com/veloren/veloren", branch = "master" } veloren-client = { git = "https://gitlab.com/veloren/veloren", branch = "master" }
veloren-world = { git = "https://gitlab.com/veloren/veloren", branch = "master" } veloren-world = { git = "https://gitlab.com/veloren/veloren", branch = "master" }
veloren-client-i18n = { git = "https://gitlab.com/veloren/veloren", branch = "master" }
toml = "0.8.14" toml = "0.8.14"
serde = { version = "1.0.203", features = ["derive"] } serde = { version = "1.0.203", features = ["derive"] }
log = "0.4.22" log = "0.4.22"

View File

@ -2,7 +2,9 @@
A bot that buys, sells and trades with players. A bot that buys, sells and trades with players.
The bot is containerized and can be run without compiling or building anything. Alternatively, you can clone this repository and build the image yourself or build the binary directly with Cargo if you are familiar with Rust. The bot is containerized and can be run without compiling or building anything. Alternatively, you
can clone this repository and build the image yourself or build the binary directly with Cargo if
you are familiar with Rust.
## Prerequisites ## Prerequisites
@ -10,7 +12,8 @@ You must have either [Docker](docker.com) or [Podman](podman.io) installed.
## Usage ## Usage
All of these steps can be done with Docker but Podman is shown for the examples. If you use Docker, just replace "podman" with "docker" in your commands. All of these steps can be done with Docker but Podman is shown for the examples. If you use
Docker, just replace "podman" with "docker" in your commands.
### Setup ### Setup
@ -30,7 +33,8 @@ Then create a secret to pass the file securely to the container.
podman secret create secrets.toml secrets.toml podman secret create secrets.toml secrets.toml
``` ```
You will also need a "config.toml" and it needs it be in a "config" directory that can be mounted to the container: You will also need a "config.toml" and it needs it be in a "config" directory that can be mounted
to the container:
```toml ```toml
# config/config.toml # config/config.toml
@ -71,14 +75,18 @@ Clone this repository. From the project root:
podman build . -t trade_bot podman build . -t trade_bot
``` ```
Then follow the [above](#running) steps with the tag "trade_bot" instead of "git.jeffa.io/jeff/trade_bot". Then follow the [above](#running) steps with the tag "trade_bot" instead of
"git.jeffa.io/jeff/trade_bot".
### In-Game Commands ### In-Game Commands
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
- `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. the search term.
- `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) - `pos [x] [y] [z]`: Admin-only, sets the bot's desired position where it will try to stand (must
- `orientation [0-360]`: Admin-only, sets the bot's desired orientation (or facing direction) be close to the bot's current position)
- `ori [0-360]`: Admin-only, sets the bot's desired orientation (or facing direction)

View File

@ -16,9 +16,15 @@ use std::{
use tokio::runtime::Runtime; use tokio::runtime::Runtime;
use vek::Quaternion; use vek::Quaternion;
use veloren_client::{addr::ConnectionArgs, Client, Event as VelorenEvent, SiteInfoRich, WorldExt}; use veloren_client::{addr::ConnectionArgs, Client, Event as VelorenEvent, SiteInfoRich, WorldExt};
use veloren_client_i18n::Localization;
use veloren_common::{ use veloren_common::{
clock::Clock, clock::Clock,
comp::{invite::InviteKind, item::ItemDefinitionIdOwned, ChatType, ControllerInputs, Ori, Pos}, comp::{
invite::InviteKind,
item::{ItemDef, ItemDefinitionIdOwned, ItemI18n},
slot::InvSlotId,
ChatType, ControllerInputs, ItemKey, Ori, Pos,
},
outcome::Outcome, outcome::Outcome,
time::DayPeriod, time::DayPeriod,
trade::{PendingTrade, TradeAction, TradeResult}, trade::{PendingTrade, TradeAction, TradeResult},
@ -144,7 +150,8 @@ impl Bot {
/// processing trade actions. /// processing trade actions.
/// ///
/// This function should be modified with care. In addition to being the bot's main loop, it /// 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. /// 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
@ -221,34 +228,6 @@ impl Bot {
let command = split_content.next().unwrap_or_default(); let command = split_content.next().unwrap_or_default();
let price_correction_message = "Use the format 'price [search_term]'"; let price_correction_message = "Use the format 'price [search_term]'";
let correction_message = match command { let correction_message = match command {
"price" => {
for item_name in split_content {
self.send_price_info(&sender, &item_name.to_lowercase())?;
}
None
}
"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())?;
log::info!("Sorting inventory {sort_count} times");
self.sort_count = sort_count;
} else {
self.client.sort_inventory();
log::info!("Sorting inventory once");
}
None
} else {
Some(price_correction_message)
}
}
"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::info!("Providing admin access"); log::info!("Providing admin access");
@ -263,7 +242,40 @@ impl Bot {
Some(price_correction_message) Some(price_correction_message)
} }
} }
"position" => { "announce" => {
if self.is_user_admin(&sender)? {
self.handle_announcement()?;
self.last_announcement = Instant::now();
None
} else {
Some(price_correction_message)
}
}
"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 'orientation [0-360]'")
}
} else {
Some(price_correction_message)
}
}
"price" => {
for item_name in split_content {
self.send_price_info(&sender, &item_name)?;
}
None
}
"pos" => {
if self.is_user_admin(&sender)? { if self.is_user_admin(&sender)? {
if let (Some(x), Some(y), Some(z)) = ( if let (Some(x), Some(y), Some(z)) = (
split_content.next(), split_content.next(),
@ -286,17 +298,23 @@ impl Bot {
Some(price_correction_message) Some(price_correction_message)
} }
} }
"orientation" => { "sort" => {
if self.is_user_admin(&sender)? { if self.is_user_admin(&sender)? {
if let Some(orientation) = split_content.next() { if let Some(sort_count) = split_content.next() {
self.orientation = orientation let sort_count = sort_count
.parse::<f32>() .parse::<u8>()
.map_err(|error| error.to_string())?; .map_err(|error| error.to_string())?;
None log::info!("Sorting inventory {sort_count} times");
self.sort_count = sort_count;
} else { } else {
Some("Use the format 'orientation [0-360]'") self.client.sort_inventory();
log::info!("Sorting inventory once");
} }
None
} else { } else {
Some(price_correction_message) Some(price_correction_message)
} }
@ -445,7 +463,7 @@ impl Bot {
/// Manage an active trade. /// Manage an active trade.
/// ///
/// This is a rather complex function that should be modified with care. The bot uses its buy /// 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 /// and sell prices to determine an item's value and determines the total value of each side of
/// the trade. Coins are hard-coded to have a value of 1 each. /// the trade. Coins are hard-coded to have a value of 1 each.
/// ///
/// The bot's trading logic is as follows: /// The bot's trading logic is as follows:
@ -455,11 +473,13 @@ impl Bot {
/// 3. If their offer includes items I am not buying, 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. /// 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: /// 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. /// 1. If they are offering coins, remove them to balance.
/// 2. If they are not offering coins, add mine 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: /// 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. /// 1. If I am offering coins, remove them to balance.
/// 2. If I am not offering coins, add theirs to balance. /// 2. If I am not offering coins, add theirs to balance.
///
/// See the inline comments for more details.
#[allow(clippy::comparison_chain)] #[allow(clippy::comparison_chain)]
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() {
@ -548,7 +568,7 @@ impl Bot {
} }
}); });
let mut my_items_to_remove = Vec::new(); let mut my_item_to_remove = None;
for (slot_id, amount) in my_offer { for (slot_id, amount) in my_offer {
let item = my_inventory.get(*slot_id).ok_or("Failed to get item")?; let item = my_inventory.get(*slot_id).ok_or("Failed to get item")?;
@ -559,11 +579,11 @@ impl Bot {
} }
if !self.sell_prices.contains_key(&item_id) { if !self.sell_prices.contains_key(&item_id) {
my_items_to_remove.push((*slot_id, *amount)); my_item_to_remove = Some((slot_id, amount));
} }
} }
let mut their_items_to_remove = Vec::new(); let mut their_item_to_remove = None;
for (slot_id, amount) in their_offer { for (slot_id, amount) in their_offer {
let item = their_inventory.get(*slot_id).ok_or("Failed to get item")?; let item = their_inventory.get(*slot_id).ok_or("Failed to get item")?;
@ -574,7 +594,7 @@ impl Bot {
} }
if !self.buy_prices.contains_key(&item_id) { if !self.buy_prices.contains_key(&item_id) {
their_items_to_remove.push((*slot_id, *amount)); their_item_to_remove = Some((slot_id, amount));
} }
} }
@ -600,32 +620,38 @@ impl Bot {
drop(inventories); drop(inventories);
if !my_items_to_remove.is_empty() { // Before running any actual trade logic, remove items that are not for sale or not being
for (item, quantity) in my_items_to_remove { // purchased. End this trade action if an item was removed.
self.client.perform_trade_action(TradeAction::RemoveItem {
item, if let Some((slot_id, quantity)) = my_item_to_remove {
quantity, self.client.perform_trade_action(TradeAction::RemoveItem {
ours: true, item: *slot_id,
}); quantity: *quantity,
} ours: true,
});
return Ok(()); return Ok(());
} }
if !their_items_to_remove.is_empty() { if let Some((slot_id, quantity)) = their_item_to_remove {
for (item, quantity) in their_items_to_remove { self.client.perform_trade_action(TradeAction::RemoveItem {
self.client.perform_trade_action(TradeAction::RemoveItem { item: *slot_id,
item, quantity: *quantity,
quantity, ours: false,
ours: false, });
});
}
return Ok(()); return Ok(());
} }
let difference = their_offered_items_value - my_offered_items_value; let difference = their_offered_items_value - my_offered_items_value;
// The if/else statements below implement the bot's main feature: buying, selling and
// trading items according to the values set in the configuration file. In the case that
// either the bot or the other player does not have any coins, the bot will not send an
// error and the trade will remain unbalanced. In the case that we try to add more coins
// than are available, the server will just add all of the available coins and the trade
// will remain unbalanced.
// If the trade is balanced // If the trade is balanced
if difference == 0 { if difference == 0 {
self.previous_offer = Some(item_offers); self.previous_offer = Some(item_offers);