From 91cbe84f4da73d08a18039c5a309c9b280b58ce7 Mon Sep 17 00:00:00 2001 From: Jeff Date: Mon, 15 Jul 2024 22:18:41 -0400 Subject: [PATCH] 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. --- Cargo.lock | 146 +++++++++++++++++++++++++++++++++++++++++++++++++++++ Cargo.toml | 1 + README.md | 24 ++++++--- src/bot.rs | 146 +++++++++++++++++++++++++++++++---------------------- 4 files changed, 249 insertions(+), 68 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 1246bc9..739dc9a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -590,6 +590,12 @@ version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e8566979429cf69b49a5c740c60791108e86440e8be149bbea4fe54d2c32d6e2" +[[package]] +name = "deunicode" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "339544cc9e2c4dc3fc7149fd630c5f22263a4fdf18a98afd0075784968b5cf00" + [[package]] name = "digest" version = "0.10.7" @@ -621,6 +627,17 @@ dependencies = [ "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]] name = "divrem" version = "1.0.0" @@ -793,6 +810,47 @@ dependencies = [ "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]] name = "fnv" version = "1.0.7" @@ -1255,6 +1313,24 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" 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]] name = "ipconfig" version = "0.3.2" @@ -2225,6 +2301,21 @@ dependencies = [ "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]] name = "semver" version = "1.0.23" @@ -2537,6 +2628,15 @@ dependencies = [ "slab", ] +[[package]] +name = "tinystr" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9117f5d4db391c1cf6927e7bea3db74b9a1c1add8f7eda9ffd5364f40f57b82f" +dependencies = [ + "displaydoc", +] + [[package]] name = "tinyvec" version = "1.6.0" @@ -2732,6 +2832,7 @@ dependencies = [ "toml", "vek", "veloren-client", + "veloren-client-i18n", "veloren-common", "veloren-common-net", "veloren-world", @@ -2768,6 +2869,15 @@ dependencies = [ "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]] name = "typenum" version = "1.17.0" @@ -2780,6 +2890,24 @@ version = "0.10.4" source = "registry+https://github.com/rust-lang/crates.io-index" 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]] name = "unicode-bidi" version = "0.3.15" @@ -2879,6 +3007,24 @@ dependencies = [ "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]] name = "veloren-common" version = "0.10.0" diff --git a/Cargo.toml b/Cargo.toml index 1b890ec..f1cedac 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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-client = { 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" serde = { version = "1.0.203", features = ["derive"] } log = "0.4.22" diff --git a/README.md b/README.md index 19ab130..af3c792 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,9 @@ 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 @@ -10,7 +12,8 @@ You must have either [Docker](docker.com) or [Podman](podman.io) installed. ## 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 @@ -30,7 +33,8 @@ Then create a secret to pass the file securely to the container. 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 # config/config.toml @@ -71,14 +75,18 @@ Clone this repository. From the project root: 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 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. -- `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. +- `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 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. -- `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) +- `pos [x] [y] [z]`: Admin-only, sets the bot's desired position where it will try to stand (must + be close to the bot's current position) +- `ori [0-360]`: Admin-only, sets the bot's desired orientation (or facing direction) diff --git a/src/bot.rs b/src/bot.rs index e84f9aa..53ea660 100644 --- a/src/bot.rs +++ b/src/bot.rs @@ -16,9 +16,15 @@ use std::{ use tokio::runtime::Runtime; use vek::Quaternion; use veloren_client::{addr::ConnectionArgs, Client, Event as VelorenEvent, SiteInfoRich, WorldExt}; +use veloren_client_i18n::Localization; use veloren_common::{ 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, time::DayPeriod, trade::{PendingTrade, TradeAction, TradeResult}, @@ -144,7 +150,8 @@ impl Bot { /// processing trade actions. /// /// 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> { let veloren_events = self .client @@ -221,34 +228,6 @@ impl Bot { let command = split_content.next().unwrap_or_default(); let price_correction_message = "Use the format 'price [search_term]'"; 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::() - .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" => { if self.is_user_admin(&sender)? && !self.client.is_trading() { log::info!("Providing admin access"); @@ -263,7 +242,40 @@ impl Bot { 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::() + .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 let (Some(x), Some(y), Some(z)) = ( split_content.next(), @@ -286,17 +298,23 @@ impl Bot { Some(price_correction_message) } } - "orientation" => { + "sort" => { if self.is_user_admin(&sender)? { - if let Some(orientation) = split_content.next() { - self.orientation = orientation - .parse::() + if let Some(sort_count) = split_content.next() { + let sort_count = sort_count + .parse::() .map_err(|error| error.to_string())?; - None + log::info!("Sorting inventory {sort_count} times"); + + self.sort_count = sort_count; } else { - Some("Use the format 'orientation [0-360]'") + self.client.sort_inventory(); + + log::info!("Sorting inventory once"); } + + None } else { Some(price_correction_message) } @@ -445,7 +463,7 @@ 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 + /// 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 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. /// 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. + /// 1. If they are offering coins, remove them 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. + /// 1. If I am offering coins, remove them to balance. /// 2. If I am not offering coins, add theirs to balance. + /// + /// See the inline comments for more details. #[allow(clippy::comparison_chain)] fn handle_trade(&mut self, trade: PendingTrade) -> Result<(), String> { 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 { 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) { - 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 { 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) { - their_items_to_remove.push((*slot_id, *amount)); + their_item_to_remove = Some((slot_id, amount)); } } @@ -600,32 +620,38 @@ impl Bot { drop(inventories); - 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, - }); - } + // 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, + }); return Ok(()); } - 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, - }); - } + if let Some((slot_id, quantity)) = their_item_to_remove { + self.client.perform_trade_action(TradeAction::RemoveItem { + item: *slot_id, + quantity: *quantity, + ours: false, + }); return Ok(()); } 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 difference == 0 { self.previous_offer = Some(item_offers);