From 6436bd9f2ca0efe7189a60bf1a0acf7be9b7a067 Mon Sep 17 00:00:00 2001 From: wrk Date: Sat, 27 May 2023 09:37:54 +0000 Subject: [PATCH] initial commit --- .gitignore | 4 + Cargo.toml | 6 + README.md | 3 + drugwars/Cargo.toml | 13 + drugwars/src/admin.rs | 126 ++++ drugwars/src/api.rs | 463 ++++++++++++++ drugwars/src/config.rs | 27 + drugwars/src/dealer.rs | 212 +++++++ drugwars/src/definitions.rs | 329 ++++++++++ drugwars/src/drug_wars.rs | 1200 +++++++++++++++++++++++++++++++++++ drugwars/src/error.rs | 135 ++++ drugwars/src/main.rs | 342 ++++++++++ drugwars/src/render.rs | 690 ++++++++++++++++++++ drugwars/src/renderer.rs | 255 ++++++++ drugwars/src/save.rs | 199 ++++++ drugwars/src/utils.rs | 227 +++++++ drugwars_config.yaml | 200 ++++++ irc/Cargo.toml | 11 + irc/src/builder.rs | 193 ++++++ irc/src/config.rs | 43 ++ irc/src/format.rs | 67 ++ irc/src/irc_command.rs | 242 +++++++ irc/src/lib.rs | 679 ++++++++++++++++++++ irc/src/privmsg.rs | 33 + irc_config.example.yaml | 21 + 25 files changed, 5720 insertions(+) create mode 100644 .gitignore create mode 100644 Cargo.toml create mode 100644 README.md create mode 100644 drugwars/Cargo.toml create mode 100644 drugwars/src/admin.rs create mode 100644 drugwars/src/api.rs create mode 100644 drugwars/src/config.rs create mode 100644 drugwars/src/dealer.rs create mode 100644 drugwars/src/definitions.rs create mode 100644 drugwars/src/drug_wars.rs create mode 100644 drugwars/src/error.rs create mode 100644 drugwars/src/main.rs create mode 100644 drugwars/src/render.rs create mode 100644 drugwars/src/renderer.rs create mode 100644 drugwars/src/save.rs create mode 100644 drugwars/src/utils.rs create mode 100644 drugwars_config.yaml create mode 100644 irc/Cargo.toml create mode 100644 irc/src/builder.rs create mode 100644 irc/src/config.rs create mode 100644 irc/src/format.rs create mode 100644 irc/src/irc_command.rs create mode 100644 irc/src/lib.rs create mode 100644 irc/src/privmsg.rs create mode 100644 irc_config.example.yaml diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..87125fd --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +/target +/Cargo.lock +save.yaml +irc_config.yaml diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..b01bd63 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,6 @@ +[workspace] + +members = [ + "irc", + "drugwars" +] \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..b5ae852 --- /dev/null +++ b/README.md @@ -0,0 +1,3 @@ +# Drugwars + +copy `irc_config.example.yaml` to `irc_config.yaml` and edit it before starting the bot. diff --git a/drugwars/Cargo.toml b/drugwars/Cargo.toml new file mode 100644 index 0000000..a2e3d90 --- /dev/null +++ b/drugwars/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "drugwars" +authors = ["wrk"] +version = "0.1.0" +edition = "2021" + +[dependencies] +irc = { path = "../irc" } +serde = { version = "1.0.163", features = ["derive"] } +serde_yaml = "0.9.21" +itertools = "0.10.5" +rand = "0.8.5" +chrono = { version = "0.4.24", features = ["serde"] } diff --git a/drugwars/src/admin.rs b/drugwars/src/admin.rs new file mode 100644 index 0000000..0ea4cea --- /dev/null +++ b/drugwars/src/admin.rs @@ -0,0 +1,126 @@ +use std::str::FromStr; + +use chrono::NaiveDate; + +use crate::{ + definitions::{DealerStatus, Location}, + drug_wars::DrugWars, + error::{Error, Result}, + utils::{hl_message, pretty_print_money}, +}; + +/* + +.dealer info + + */ + +impl DrugWars { + pub fn admin_dealer(&mut self, nick: &str, arguments: &[&str]) -> Result> { + if arguments.len() < 1 { + return self.show_all_commands(); + } + + match arguments[0].to_lowercase().as_str() { + "info" => { + let dealer = self.get_dealer(arguments[1])?; + Ok(self.render_info(arguments[1], dealer)) + } + "set" => self.admin_set(nick, arguments[1], arguments[2], &arguments[3..]), + _ => Ok(vec![]), + } + } + + fn admin_set( + &mut self, + nick: &str, + dealer_nick: &str, + cmd: &str, + args: &[&str], + ) -> Result> { + if args.len() < 1 { + return Ok(self.render_command_list()); + } + + match cmd { + "money" => { + let amount = (self.get_amount_from_str::(args[0])? * 10000.) as u128; + let mut dealer = self.get_dealer_mut(dealer_nick)?; + dealer.money = amount; + Ok(hl_message( + nick, + &format!("{} has now {}", dealer_nick, pretty_print_money(amount)), + )) + } + "laundermoney" => { + let amount = (self.get_amount_from_str::(args[0])? * 10000.) as u128; + let mut dealer = self.get_dealer_mut(dealer_nick)?; + dealer.laundered_money = amount; + Ok(hl_message( + nick, + &format!( + "{} has now {} as laundered money", + dealer_nick, + pretty_print_money(amount) + ), + )) + } + + "health" => { + let amount = self.get_amount_from_str::(args[0])?; + let mut dealer = self.get_dealer_mut(dealer_nick)?; + dealer.health = amount; + Ok(hl_message( + nick, + &format!("{} has now {:.2} hp", dealer_nick, amount), + )) + } + "capacity" => { + let amount = self.get_amount_from_str::(args[0])?; + let mut dealer = self.get_dealer_mut(dealer_nick)?; + dealer.capacity = amount; + Ok(hl_message( + nick, + &format!("{} has now {} slots", dealer_nick, amount), + )) + } + "status" => { + let new_status = match args[0].to_lowercase().as_str() { + "available" => DealerStatus::Available, + "dead" => { + if args.len() < 2 { + return Ok(self.render_command_list()); + } + DealerStatus::Dead(match NaiveDate::from_str(args[1]) { + Ok(date) => date, + Err(_) => return Err(Error::UnknownDate(args[1].to_owned())), + }) + } + "flying" => DealerStatus::Flying, + _ => return Err(Error::UnknownStatus(args[0].to_string())), + }; + + let mut dealer = self.get_dealer_mut(dealer_nick)?; + dealer.status = new_status; + + Ok(hl_message( + nick, + &format!("{} has is now {:?}", dealer_nick, dealer.status), + )) + } + "location" => { + let (new_loc_name, _) = self.get_matching::(args[0])?; + let new_loc_name = new_loc_name.clone(); + + let mut dealer = self.get_dealer_mut(dealer_nick)?; + dealer.location = new_loc_name; + + Ok(hl_message( + nick, + &format!("{} is now at {}", dealer_nick, dealer.location), + )) + } + _ => Ok(vec![]), + } + } +} diff --git a/drugwars/src/api.rs b/drugwars/src/api.rs new file mode 100644 index 0000000..ea8e003 --- /dev/null +++ b/drugwars/src/api.rs @@ -0,0 +1,463 @@ +use irc::{format::IrcColor, privmsg::PrivMsg}; + +use crate::{ + dealer::Dealer, + definitions::{Item, ItemKind, Location}, + drug_wars::DrugWars, + error::{Error, Result}, + utils::{ + capacity_price, get_flight_price, get_shipping_price, hl_message, pretty_print_amount, + pretty_print_money, DealerComponent, Matchable, + }, +}; + +impl DrugWars { + pub fn register_dealer(&mut self, nick: &str) -> Result> { + if self.dealers.contains_key(nick) { + return Err(Error::DealerAlreadyRegistered); + } + + self.dealers.insert(nick.to_owned(), Dealer::random(self)); + + let dealer = self.get_dealer(nick)?; + let location = self.locations.get_mut(&dealer.location.clone()).unwrap(); + + location.blokes.insert(nick.to_owned()); + + let mut msg = PrivMsg::new(); + let msg = msg + .color(IrcColor::Purple) + .text("get rich or die tryin") + .get(); + + Ok(hl_message(nick, msg)) + } + + pub fn show_market(&self, nick: &str) -> Result> { + let dealer = self.get_dealer(nick)?; + Ok(self.render_market(nick, dealer)) + } + + pub fn show_info(&self, nick: &str) -> Result> { + let dealer = self.get_dealer(nick)?; + Ok(self.render_info(nick, dealer)) + } + + pub fn show_people(&self, nick: &str) -> Result> { + let dealer = self.get_dealer(nick)?; + Ok(self.render_people(dealer)) + } + + pub fn show_date_time(&self) -> Result> { + Ok(self.render_time()) + } + + pub fn show_all_commands(&self) -> Result> { + Ok(self.render_command_list()) + } + + pub fn leaderboard(&self) -> Result> { + Ok(self.render_leaderboard()) + } + + pub fn show_admin_commands(&self) -> Result> { + Ok(self.render_admin_command_list()) + } + + pub fn buy( + &mut self, + nick: &str, + name_str: &str, + amount_str: &str, + ) -> Result> { + let dealer = self.get_dealer_available(nick)?; + + let (name, _) = self.get_matching::(&name_str)?; + let amount = self.get_buy_amount_of::(dealer, name, amount_str)?; + + let location = self.locations.get(&dealer.location).unwrap(); + let market = location.get_market::(); + + if !market.contains_key(name) { + return Err(Error::NoElementAtMarket(name.to_owned())); + } + + if dealer.get_total_owned_local::() + amount > dealer.capacity { + return Err(Error::NotEnoughCapacity); + } + + let elem_at_market = market.get(name).unwrap(); + + if elem_at_market.supply < amount { + return Err(Error::NotEnoughSupply(name.to_owned())); + } + + let total_price = elem_at_market.price * amount as u128; + + if total_price > dealer.money { + return Err(Error::NotEnoughMoney); + } + + self._buy::(nick, &name.clone(), amount, elem_at_market.price) + } + + pub fn sell( + &mut self, + nick: &str, + name_str: &str, + amount_str: &str, + ) -> Result> { + let dealer = self.get_dealer_available(nick)?; + + let (name, _) = self.get_matching::(&name_str)?; + + let amount = self.get_sell_amount_of::(dealer, name, amount_str)?; + let location = self.locations.get(&dealer.location).unwrap(); + let market = T::get_market_at(location); + + if !market.contains_key(name) { + return Err(Error::NoElementAtMarket(name.to_owned())); + } + + let elem_at_market = market.get(name).unwrap(); + if elem_at_market.demand < amount { + return Err(Error::NotEnoughDemand(name.to_owned())); + } + + let owned_local = dealer.get_owned_local::(); + + if !owned_local.contains_key(name) { + return Err(Error::NoElementOwned(name.to_owned())); + } + + let owned_element = owned_local.get(name).unwrap(); + + if owned_element.amount < amount { + return Err(Error::NotEnoughElementOwned(name.to_owned())); + } + + self._sell::(nick, &name.clone(), amount) + } + + pub fn give_money( + &mut self, + nick: &str, + amount_str: &str, + bloke_nick: &str, + ) -> Result> { + let dealer = self.get_dealer_available(nick)?; + + let money: u128 = self.get_money_amount(dealer, amount_str)?; + + self.get_dealer(bloke_nick)?; + self._give_money(nick, bloke_nick, money) + } + + pub fn give( + &mut self, + nick: &str, + name_str: &str, + amount_str: &str, + bloke_nick: &str, + ) -> Result> { + let dealer = self.get_dealer_available(nick)?; + + let bloke = self.get_dealer(bloke_nick)?; + + if dealer.location != bloke.location { + return Err(Error::BlokeNotHere(bloke_nick.to_owned())); + } + + let (name, _) = self.get_matching::(&name_str)?; + + let owned_element_local = dealer.get_owned_local::(); + + if !owned_element_local.contains_key(name) { + return Err(Error::NoElementOwned(name.to_owned())); + } + + let owned_element = owned_element_local.get(name).unwrap(); + + let amount = self.get_give_amount_of::(dealer, bloke, name, amount_str)?; + + if owned_element.amount < amount { + return Err(Error::NotEnoughElementOwned(name.to_owned())); + } + + if bloke.get_total_owned_local::() + amount > bloke.capacity { + return Err(Error::BlokeNotEnoughCapacity(bloke_nick.to_owned())); + } + + self._give::(nick, bloke_nick, &name.clone(), amount) + } + + pub fn check_flight_prices(&self, nick: &str) -> Result> { + let dealer = self.get_dealer_available(nick)?; + Ok(self.render_prices_from(&dealer.location)) + } + + pub fn fly_to(&mut self, nick: &str, destination_str: &str) -> Result> { + let dealer = self.get_dealer_available(nick)?; + + let (destination_name, destination) = self.get_matching(&destination_str)?; + + let current_location = self.locations.get(&dealer.location).unwrap(); + let price = get_flight_price(current_location, destination); + + if dealer.money < price { + return Err(Error::NotEnoughMoney); + } + + self._fly_to(nick, &destination_name.clone()) + } + + pub fn check_capacity_price(&mut self, nick: &str, amount_str: &str) -> Result> { + let dealer = self.get_dealer_available(nick)?; + + let amount = self.get_amount_from_str(amount_str)?; + let price = capacity_price(dealer.capacity, amount)?; + + let mut msg = PrivMsg::new(); + let msg = msg + .text("it will cost you ") + .color(IrcColor::Green) + .text(&pretty_print_money(price)) + .reset() + .text(" to buy ") + .color(IrcColor::Yellow) + .text(&pretty_print_amount(amount)) + .reset() + .text(" slots.") + .get(); + + Ok(hl_message(nick, msg)) + } + + pub fn check_shipping_price( + &mut self, + nick: &str, + name_str: &str, + amount_str: &str, + destination_str: &str, + ) -> Result> { + let dealer = self.get_dealer_available(nick)?; + + let (name, _) = self.get_matching::(&name_str)?; + + if !dealer.get_owned_local::().contains_key(name) { + return Err(Error::NoElementOwned(name.to_owned())); + } + + let (destination_name, destination) = self.get_matching::(&destination_str)?; + + if dealer.location == *destination_name { + return Err(Error::ShipCurrentLocation); + } + + let amount = self.get_ship_amount_of::(dealer, destination_name, name, amount_str)?; + + let target_remaining_capacity = dealer.get_remaining_capacity_at::(destination_name); + + if target_remaining_capacity < amount { + return Err(Error::NotEnoughCapacityAt(destination_name.to_owned())); + } + + let dealer_location = self.locations.get(&dealer.location).unwrap(); + + let price = get_shipping_price(dealer_location, destination, amount); + + let mut msg = PrivMsg::new(); + let msg = msg + .text("you'll have to pay ") + .color(IrcColor::Green) + .text(&pretty_print_money(price)) + .reset() + .text(" to ship ") + .color(IrcColor::Yellow) + .text(&format!("{} {}", pretty_print_amount(amount), name)) + .reset() + .text(" to ") + .color(IrcColor::Purple) + .text(&destination_name) + .get(); + + Ok(hl_message(nick, msg)) + } + + pub fn ship( + &mut self, + nick: &str, + name_str: &str, + amount_str: &str, + destination_str: &str, + ) -> Result> { + let dealer = self.get_dealer_available(nick)?; + + let (name, _) = self.get_matching::(&name_str)?; + + if !dealer.get_owned_local::().contains_key(name) { + return Err(Error::NoElementOwned(name.to_owned())); + } + + let (destination_name, destination) = self.get_matching::(&destination_str)?; + + if dealer.location == *destination_name { + return Err(Error::ShipCurrentLocation); + } + + let amount = self.get_ship_amount_of::(dealer, &destination_name, name, amount_str)?; + + let owned_local = dealer.get_owned_local::(); + + if !owned_local.contains_key(name) { + return Err(Error::NoElementOwned(name.to_owned())); + } + + let element = owned_local.get(name).unwrap(); + + if element.amount < amount { + return Err(Error::NotEnoughElementOwned(name.to_owned())); + } + + let target_remaining_capacity = dealer.get_remaining_capacity_at::(destination_name); + + if target_remaining_capacity < amount { + return Err(Error::NotEnoughCapacityAt(destination_name.to_owned())); + } + + let dealer_location = self.locations.get(&dealer.location).unwrap(); + + let price = get_shipping_price(dealer_location, destination, amount); + + if dealer.money < price { + return Err(Error::NotEnoughMoney); + } + + self._ship::(nick, &name.clone(), amount, &destination_name.clone()) + } + + pub fn buy_capacity(&mut self, nick: &str, amount_str: &str) -> Result> { + let dealer = self.get_dealer_available(nick)?; + + let amount = self.get_amount_from_str(amount_str)?; + + let price = capacity_price(dealer.capacity, amount)?; + + if dealer.money < price { + return Err(Error::NotEnoughMoney); + } + + self._buy_capacity(nick, amount) + } + + pub fn attack( + &mut self, + nick: &str, + target_nick: &str, + weapon_str: &str, + ) -> Result> { + let dealer = self.get_dealer_available(nick)?; + + if dealer.has_attacked { + return Err(Error::AlreadyAttacked); + } + + let Ok(target_dealer) = self.get_dealer(target_nick) else { + return Err(Error::DealerNotFound(target_nick.to_owned())); + }; + + if !target_dealer.available() { + return Err(Error::CantAttackDealer( + target_nick.to_owned(), + target_dealer.status.clone(), + )); + } + + if dealer.location != target_dealer.location { + return Err(Error::NotSameLocation(target_nick.to_owned())); + } + + let (weapon_name, weapon) = self.get_matching::(weapon_str)?; + + let ItemKind::Weapon(w) = &weapon.kind else { + return Err(Error::ItemNotWeapon(weapon_name.to_owned())); + }; + + dealer.can_use_weapon(weapon_name, w.ammo.as_deref())?; + + self._attack(nick, target_nick, weapon_str) + } + + pub fn heal(&mut self, nick: &str) -> Result> { + self.get_dealer_available(nick)?; + + let dealer = self.get_dealer_mut(nick)?; + + if dealer.health == 100. { + return Err(Error::AlreadyFullHealth); + } + + dealer.health = 100.; + let cost = dealer.money / 3; + + dealer.money -= cost; + + let mut msg = PrivMsg::new(); + let msg = msg + .text("you restored all your health for ") + .color(IrcColor::Green) + .text(&pretty_print_money(cost)) + .get(); + + Ok(hl_message(nick, msg)) + } + + pub fn loot(&mut self, nick: &str, target_nick: &str) -> Result> { + let dealer = self.get_dealer_available(nick)?; + + let Ok(target_dealer) = self.get_dealer(target_nick) else { + return Err(Error::DealerNotFound(target_nick.to_owned())); + }; + + if !target_dealer.is_dead() { + return Err(Error::DealerNotDead(target_nick.to_owned())); + } + + if dealer.location != target_dealer.location { + return Err(Error::NotSameLocation(target_nick.to_owned())); + } + + if target_dealer.looters.contains(nick) { + return Err(Error::AlreadyLooted(target_nick.to_owned())); + } + + self._loot(nick, target_nick) + } + + pub fn launder(&mut self, nick: &str, amount_str: &str) -> Result> { + let dealer = self.get_dealer_available(nick)?; + + let (laundered, amount) = self.get_laundering_amount(dealer, amount_str)?; + + let dealer = self.get_dealer_mut(nick)?; + dealer.money -= amount; + dealer.laundered_money += laundered; + + let mut msg = PrivMsg::new(); + let msg = msg + .text("You laundered ") + .color(IrcColor::Green) + .text(&pretty_print_money(amount)) + .reset() + .text(" into ") + .color(IrcColor::Green) + .text(&pretty_print_money(laundered)) + .reset() + .text(" with a fee of ") + .color(IrcColor::Yellow) + .text(&format!("{:.2}%", self.laundering_fees * 100.)) + .get(); + + Ok(hl_message(nick, msg)) + } +} diff --git a/drugwars/src/config.rs b/drugwars/src/config.rs new file mode 100644 index 0000000..cfc088f --- /dev/null +++ b/drugwars/src/config.rs @@ -0,0 +1,27 @@ +use std::{fs::File, io::Read}; + +use serde::Deserialize; +use serde_yaml::{Mapping, Sequence}; + +#[derive(Deserialize)] +pub struct DrugWarsConfig { + pub settings: Mapping, + pub locations: Sequence, + pub drugs: Sequence, + pub items: Mapping, + pub messages: Mapping, + pub config_path: Option, +} + +impl DrugWarsConfig { + pub fn from_file(path: &str) -> std::io::Result { + let mut file = File::open(path)?; + let mut contents = String::new(); + file.read_to_string(&mut contents)?; + + let mut config: DrugWarsConfig = serde_yaml::from_str(&contents).unwrap(); + config.config_path = Some(path.to_owned()); + + Ok(config) + } +} diff --git a/drugwars/src/dealer.rs b/drugwars/src/dealer.rs new file mode 100644 index 0000000..cb5cd70 --- /dev/null +++ b/drugwars/src/dealer.rs @@ -0,0 +1,212 @@ +use std::collections::{HashMap, HashSet}; + +use itertools::Itertools; +use rand::{seq::IteratorRandom, RngCore}; +use serde::{Deserialize, Serialize}; + +use crate::{ + definitions::{Armor, DealerStatus, Item, ItemKind, OwnedElement}, + drug_wars::DrugWars, + error::{Error, Result}, + utils::DealerComponent, +}; + +#[derive(Serialize, Deserialize, Clone)] +pub struct Dealer { + pub has_attacked: bool, + pub health: f32, + pub money: u128, + pub laundered_money: u128, + pub location: String, + pub capacity: usize, + pub owned_drugs: HashMap>, + pub owned_items: HashMap>, + pub cartel_payroll: u128, + pub cartel_health: f32, + pub status: DealerStatus, + pub looters: HashSet, +} + +impl Dealer { + pub fn random(drug_wars: &DrugWars) -> Self { + let mut rng = drug_wars.rng.clone(); + + let mut owned_drugs = HashMap::default(); + let mut owned_items = HashMap::default(); + + for (name, _) in &drug_wars.locations { + owned_drugs.insert(name.clone(), HashMap::default()); + owned_items.insert(name.clone(), HashMap::default()); + } + + let location_name = drug_wars.locations.keys().choose(&mut rng).unwrap(); + + Self { + has_attacked: false, + health: 100., + money: 1_000 * 10_000, + laundered_money: 0, + location: location_name.clone(), + capacity: 10, + owned_drugs, + owned_items, + cartel_payroll: 0, + cartel_health: 0., + status: DealerStatus::Available, + looters: HashSet::default(), + } + } + + pub fn available(&self) -> bool { + self.status == DealerStatus::Available + } + + pub fn is_dead(&self) -> bool { + match self.status { + DealerStatus::Dead(_) => true, + _ => false, + } + } + + pub fn get_owned_local(&self) -> &HashMap { + T::get_elements_at(self, &self.location) + } + + pub fn get_total_owned_local(&self) -> usize { + self.get_total_owned_at::(&self.location) + } + + pub fn get_total_owned_at(&self, location: &str) -> usize { + T::get_elements_at(self, &location) + .iter() + .map(|(_, e)| e.amount) + .sum() + } + + pub fn get_remaining_capacity_local(&self) -> usize { + self.get_remaining_capacity_at::(&self.location) + } + + pub fn get_remaining_capacity_at(&self, location: &str) -> usize { + self.capacity - self.get_total_owned_at::(location) + } + + pub fn print_status(&self) -> &str { + match self.status { + DealerStatus::Available => "Available", + DealerStatus::Flying => "Flying", + DealerStatus::Dead(_) => "Dead", + } + } + + pub fn add_local(&mut self, name: &str, amount: usize, bought_at: u128) { + let location = self.location.clone(); + self.add_at::(&location, name, amount, bought_at) + } + + pub fn add_at( + &mut self, + location: &str, + name: &str, + amount: usize, + bought_at: u128, + ) { + if amount == 0 { + return; + } + + let owned_elements = T::get_elements_at_mut(self, location); + + let owned_element = match owned_elements.entry(name.to_owned()) { + std::collections::hash_map::Entry::Occupied(o) => o.into_mut(), + std::collections::hash_map::Entry::Vacant(v) => v.insert(OwnedElement::default()), + }; + + let average_price = (owned_element.amount as u128 * owned_element.bought_at + + amount as u128 * bought_at) + / (owned_element.amount as u128 + amount as u128); + + owned_element.amount += amount; + owned_element.bought_at = average_price; + } + + pub fn sub_local(&mut self, name: &str, amount: usize) { + let location = self.location.clone(); + self.sub_at::(&location, name, amount); + } + + pub fn sub_at(&mut self, location: &str, name: &str, amount: usize) { + if amount == 0 { + return; + } + + let owned_elements = T::get_elements_at_mut(self, location); + + let owned_element = match owned_elements.entry(name.to_owned()) { + std::collections::hash_map::Entry::Occupied(o) => o.into_mut(), + std::collections::hash_map::Entry::Vacant(v) => v.insert(OwnedElement::default()), + }; + + owned_element.amount -= amount; + + if owned_element.amount <= 0 { + owned_elements.remove(name); + } + } + + pub fn can_use_weapon(&self, weapon_name: &str, maybe_ammo: Option<&str>) -> Result<()> { + let local_owned = self.owned_items.get(&self.location).unwrap(); + + let Some(_) = local_owned.get(weapon_name) else { + return Err(Error::WeaponNotFound(weapon_name.to_owned())); + }; + + if maybe_ammo.is_some() { + let Some(ammo) = local_owned.get(&maybe_ammo.unwrap().to_owned()) else { + return Err(Error::NoElementOwned(maybe_ammo.unwrap().to_owned())); + }; + + if ammo.amount < 1 { + return Err(Error::NotEnoughElementOwned(maybe_ammo.unwrap().to_owned())); + } + } + + Ok(()) + } + + pub fn get_best_armor(&self, drug_wars: &DrugWars) -> Option<(String, Armor)> { + let local_owned = self.owned_items.get(&self.location).unwrap(); + + local_owned + .iter() + .filter_map(|(item_str, _)| { + let item_str = item_str.replace(" ", "").to_lowercase(); + + let Ok((item_name, item)) = drug_wars.get_matching::(&item_str) else { + return None + }; + + match &item.kind { + ItemKind::Armor(armor) => Some((item_name.to_owned(), armor.clone())), + _ => None, + } + }) + .sorted_by_key(|(_, armor)| (armor.block * 1000.) as u32) + .rev() + .last() + } + + pub fn get_random( + &self, + rng: &mut dyn RngCore, + ) -> Option<(String, OwnedElement)> { + let owned_elements = self.get_owned_local::(); + + let element = owned_elements.iter().choose(rng); + if element.is_none() { + return None; + } + + Some((element.unwrap().0.clone(), element.unwrap().1.clone())) + } +} diff --git a/drugwars/src/definitions.rs b/drugwars/src/definitions.rs new file mode 100644 index 0000000..e71e426 --- /dev/null +++ b/drugwars/src/definitions.rs @@ -0,0 +1,329 @@ +use std::collections::{HashMap, HashSet}; + +use crate::{ + drug_wars::DrugWars, + utils::{DealerComponent, Matchable}, +}; +use chrono::NaiveDate; +use itertools::Itertools; +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, Serialize, Deserialize)] +pub enum MessageKind { + RumorUpHere, + RumorDownHere, + RumorUpAt, + RumorDownAt, + Welcome, + PriceUp, + PriceUpEnd, + PriceDown, + PriceDownEnd, +} + +#[derive(Clone, Copy, Serialize, Deserialize)] +pub struct Drug { + pub nominal_price: u128, +} + +impl Matchable for Drug { + fn get_matching_elements<'a>( + drug_wars: &'a DrugWars, + name: &'a str, + ) -> Vec<(&'a String, &'a Self)> + where + Self: Sized, + { + let name = name.to_lowercase(); + drug_wars + .drugs + .iter() + .filter(|(elem_name, _)| elem_name.to_lowercase().replace(" ", "").starts_with(&name)) + .collect::>() + } +} + +impl DealerComponent for Drug { + fn get_elements_at<'a>( + dealer: &'a crate::dealer::Dealer, + location: &'a str, + ) -> &'a HashMap { + dealer.owned_drugs.get(location).unwrap() + } + + fn get_elements_at_mut<'a>( + dealer: &'a mut crate::dealer::Dealer, + location: &'a str, + ) -> &'a mut HashMap { + dealer.owned_drugs.get_mut(location).unwrap() + } + + fn get_market_at<'a>(location: &'a Location) -> &'a HashMap { + &location.drug_market + } + + fn get_market_at_mut<'a>(location: &'a mut Location) -> &'a mut HashMap { + &mut location.drug_market + } +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct Weapon { + pub ammo: Option, + pub damage: f32, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct Armor { + pub block: f32, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct NoScent { + pub capacity: usize, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub enum ItemKind { + Weapon(Weapon), + Ammo, + Armor(Armor), + NoScent(NoScent), +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct Item { + pub nominal_price: u128, + pub kind: ItemKind, +} + +impl DealerComponent for Item { + fn get_elements_at<'a>( + dealer: &'a crate::dealer::Dealer, + location: &'a str, + ) -> &'a HashMap { + dealer.owned_items.get(location).unwrap() + } + + fn get_elements_at_mut<'a>( + dealer: &'a mut crate::dealer::Dealer, + location: &'a str, + ) -> &'a mut HashMap { + dealer.owned_items.get_mut(location).unwrap() + } + + fn get_market_at<'a>(location: &'a Location) -> &'a HashMap { + &location.item_market + } + + fn get_market_at_mut<'a>(location: &'a mut Location) -> &'a mut HashMap { + &mut location.item_market + } +} + +impl Matchable for Item { + fn get_matching_elements<'a>( + drug_wars: &'a DrugWars, + name: &'a str, + ) -> Vec<(&'a String, &'a Self)> + where + Self: Sized, + { + let name = name.to_lowercase(); + drug_wars + .items + .iter() + .filter(|(elem_name, _)| elem_name.to_lowercase().replace(" ", "").starts_with(&name)) + .sorted_by_key(|k| k.0.len()) + .collect::>() + } +} + +#[derive(Default, Debug, Serialize, Deserialize, Clone)] +pub struct OwnedElement { + pub amount: usize, + pub bought_at: u128, +} + +const CHUNK: usize = 200; + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct MarketElement { + pub supply: usize, + pub demand: usize, + pub price: u128, +} + +impl MarketElement { + pub fn buy(&mut self, amount: usize) -> u128 { + let loops = amount / CHUNK; + let rest = amount % CHUNK; + + let mut price = 0; + for _ in 0..loops { + price += self.price * CHUNK as u128; + self.extract(CHUNK); + } + price += self.price * rest as u128; + self.extract(rest); + + price + } + + pub fn sell(&mut self, amount: usize) -> u128 { + let loops = amount / CHUNK; + let rest = amount % CHUNK; + + let mut price = 0; + for _ in 0..loops { + price += self.price * CHUNK as u128; + self.inject(CHUNK); + } + price += self.price * rest as u128; + self.inject(rest); + + price + } + + pub fn inject(&mut self, amount: usize) { + let deviation = self.demand as f64 - self.supply as f64; + let current_ratio = 1. + (deviation / ((self.supply as f64).max(self.demand as f64) * 1.7)); + + let f_price = (self.price as f64) / 10000.; + let unit_price = f_price / (current_ratio * 100.); + + self.supply += amount; + self.demand -= amount; + + let deviation = self.demand as f64 - self.supply as f64; + let new_ratio = 1. + (deviation / ((self.supply as f64).max(self.demand as f64) * 1.7)); + + let new_f_price = unit_price * new_ratio * 100.; + self.price = (new_f_price * 10000.) as u128; + } + + pub fn extract(&mut self, amount: usize) { + let deviation = self.demand as f64 - self.supply as f64; + let current_ratio = 1. + (deviation / ((self.supply as f64).max(self.demand as f64) * 1.7)); + + let f_price = (self.price as f64) / 10000.; + let unit_price = f_price / (current_ratio * 100.); + + self.supply -= amount; + self.demand += amount; + + let deviation = self.demand as f64 - self.supply as f64; + let new_ratio = 1. + (deviation / ((self.supply as f64).max(self.demand as f64) * 1.7)); + + let new_f_price = unit_price * new_ratio * 100.; + self.price = (new_f_price * 10000.) as u128; + } +} + +#[derive(Serialize, Deserialize, Clone)] +pub struct Location { + pub lat: f32, + pub long: f32, + pub drug_market: HashMap, + pub item_market: HashMap, + pub messages: Vec, + pub blokes: HashSet, + pub price_mods: Vec, + pub rumors: Vec, +} + +impl Matchable for Location { + fn get_matching_elements<'a>( + drug_wars: &'a DrugWars, + name: &'a str, + ) -> Vec<(&'a String, &'a Self)> + where + Self: Sized, + { + let name = name.to_lowercase(); + drug_wars + .locations + .iter() + .filter(|(elem_name, _)| elem_name.to_lowercase().contains(&name)) + .collect::>() + } +} + +impl Location { + pub fn get_market(&self) -> &HashMap { + T::get_market_at(self) + } + + pub fn get_market_mut(&mut self) -> &mut HashMap { + T::get_market_at_mut(self) + } +} + +pub struct Settings { + pub day_duration: u32, + pub current_day: NaiveDate, + pub save_path: String, + pub config_path: String, + pub width: usize, +} + +#[derive(Default, Clone, Serialize, Deserialize)] +pub struct Shipment { + pub owner: String, + pub element: String, + pub amount: usize, + pub destination: String, + pub bought_at: u128, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub enum DealerStatus { + Available, + Flying, + Dead(NaiveDate), +} + +impl std::fmt::Display for DealerStatus { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + DealerStatus::Available => write!(f, ""), + DealerStatus::Flying => write!(f, "can't do business while flying"), + DealerStatus::Dead(_) => write!(f, "can't do business while dead"), + } + } +} + +#[derive(PartialEq, Eq)] +pub enum AmountAction { + Buying, + Selling, + Shipping, + Capacity, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub enum PriceTrend { + Up, + Down, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub enum PriceModKind { + Rumor, + Spontaneous, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct PriceMod { + pub drug: String, + pub trend: PriceTrend, + pub kind: PriceModKind, +} + +#[derive(Serialize, Deserialize, Clone)] +pub struct Rumor { + pub drug: String, + pub trend: PriceTrend, + pub location: String, + pub confirmed: Option, +} diff --git a/drugwars/src/drug_wars.rs b/drugwars/src/drug_wars.rs new file mode 100644 index 0000000..fb23c4f --- /dev/null +++ b/drugwars/src/drug_wars.rs @@ -0,0 +1,1200 @@ +use std::{ + collections::{HashMap, HashSet}, + fmt::Debug, + str::FromStr, + time::SystemTime, +}; + +use chrono::{Duration, NaiveDate}; +use irc::{format::IrcColor, privmsg::PrivMsg, Irc}; +use rand::{rngs::StdRng, seq::SliceRandom, Rng, SeedableRng}; + +use crate::{ + config::DrugWarsConfig, + dealer::Dealer, + definitions::{ + Armor, DealerStatus, Drug, Item, ItemKind, Location, MarketElement, MessageKind, PriceMod, + PriceModKind, PriceTrend, Rumor, Settings, Shipment, Weapon, + }, + error::{Error, Result}, + save::DrugWarsSave, + utils::{ + calc_damage, capacity_price, get_flight_price, get_shipping_price, hl_error, hl_message, + pretty_print_amount, pretty_print_money, DealerComponent, Matchable, + }, +}; + +pub struct DrugWars { + pub settings: Settings, + pub rng: StdRng, + pub timer: SystemTime, + pub dealers: HashMap, + pub locations: HashMap, + pub flights: HashMap, + pub shipments: Vec, + pub drugs: HashMap, + pub items: HashMap, + pub messages: HashMap>, + pub laundering_fees: f32, +} + +impl From for DrugWars { + fn from(config: DrugWarsConfig) -> Self { + let mut locations = HashMap::default(); + let mut drugs = HashMap::default(); + let mut items = HashMap::default(); + let mut messages = HashMap::default(); + + for drug in config.drugs { + let name = drug.as_mapping().unwrap()["name"].as_str().unwrap(); + let price = drug.as_mapping().unwrap()["price"].as_f64().unwrap(); + drugs.insert( + name.to_owned(), + Drug { + nominal_price: (price * 10000.) as u128, + }, + ); + } + + let weapons = config.items["weapons"] + .as_sequence() + .unwrap() + .iter() + .map(|value| value.as_mapping().unwrap()) + .collect::>(); + + for weapon in weapons { + let name = weapon["name"].as_str().unwrap(); + let price = weapon["price"].as_f64().unwrap(); + let damage = weapon["damage"].as_f64().unwrap() as f32; + + let mut ammo = None; + + if weapon.contains_key("ammo") { + ammo = Some(weapon["ammo"].as_str().unwrap().to_owned()) + } + + items.insert( + name.to_owned(), + Item { + nominal_price: (price * 10000.) as u128, + kind: ItemKind::Weapon(Weapon { ammo, damage }), + }, + ); + } + + let ammos = config.items["ammos"] + .as_sequence() + .unwrap() + .iter() + .map(|value| value.as_mapping().unwrap()) + .collect::>(); + + for ammo in ammos { + let name = ammo["name"].as_str().unwrap(); + let price = ammo["price"].as_f64().unwrap(); + + items.insert( + name.to_owned(), + Item { + nominal_price: (price * 10000.) as u128, + kind: ItemKind::Ammo, + }, + ); + } + + let armors = config.items["armors"] + .as_sequence() + .unwrap() + .iter() + .map(|value| value.as_mapping().unwrap()) + .collect::>(); + + for armor in armors { + let name = armor["name"].as_str().unwrap(); + let price = armor["price"].as_f64().unwrap(); + let block = armor["block"].as_f64().unwrap() as f32; + + items.insert( + name.to_owned(), + Item { + nominal_price: (price * 10000.) as u128, + kind: ItemKind::Armor(Armor { block }), + }, + ); + } + + for location in config.locations { + let name = location.as_mapping().unwrap()["name"].as_str().unwrap(); + let lat = location.as_mapping().unwrap()["position"] + .as_mapping() + .unwrap()["lat"] + .as_f64() + .unwrap() as f32; + let long = location.as_mapping().unwrap()["position"] + .as_mapping() + .unwrap()["long"] + .as_f64() + .unwrap() as f32; + + locations.insert( + name.to_owned(), + Location { + lat, + long, + drug_market: HashMap::default(), + item_market: HashMap::default(), + messages: vec![], + blokes: HashSet::default(), + price_mods: vec![], + rumors: vec![], + }, + ); + } + + // OH LOOK ! I'M FUCKING SLEEP DEPRIVATED ! + for (val_str, enum_variant) in [ + ("price_up", MessageKind::PriceUp), + ("price_up_end", MessageKind::PriceUpEnd), + ("price_down", MessageKind::PriceDown), + ("price_down_end", MessageKind::PriceDownEnd), + ] { + let msgs = &config.messages[val_str].as_sequence().unwrap(); + for msg in *msgs { + let message_vec = messages.entry(enum_variant).or_insert_with(|| vec![]); + message_vec.push(msg.as_str().unwrap().to_owned()); + } + } + + let day_duration = config.settings["day_duration"].as_u64().unwrap() as u32; + let current_day_str = config.settings["start_day"].as_str().unwrap(); + let save_path = config.settings["save_path"].as_str().unwrap(); + let width = config.settings["width"].as_u64().unwrap(); + + let mut rng = StdRng::from_entropy(); + + let laundering_fees = rng.gen_range((0.05)..=(0.2)); + + Self { + settings: Settings { + day_duration, + current_day: NaiveDate::from_str(current_day_str).unwrap(), + save_path: save_path.to_owned(), + config_path: config.config_path.unwrap(), + width: width as usize, + }, + rng, + timer: SystemTime::now(), + dealers: HashMap::default(), + locations, + shipments: vec![], + flights: HashMap::default(), + drugs, + items, + messages, + laundering_fees, + } + } +} + +impl DrugWars { + pub fn load_config(path: &str) -> Self { + let config = DrugWarsConfig::from_file(path).unwrap(); + let Some(save_path) = config.settings["save_path"].as_str() else { + return config.into(); + }; + + match DrugWarsSave::load(save_path) { + Ok(save) => save.into(), + Err(_) => config.into(), + } + } + + pub fn init(&mut self) { + self.update_price_mods(); + self.confirm_rumors(); + + self.update_markets(); + self.generate_rumors(); + } + + fn update_price_mods(&mut self) { + for (_, location) in &mut self.locations { + location.price_mods.clear(); + + for (drug_name, _) in &self.drugs { + if self.rng.gen_bool(0.92) { + continue; + } + + match self.rng.gen_bool(1. / 2.) { + // Price down + true => location.price_mods.push(PriceMod { + drug: drug_name.clone(), + trend: PriceTrend::Down, + kind: PriceModKind::Spontaneous, + }), + // Price UP ! + false => location.price_mods.push(PriceMod { + drug: drug_name.clone(), + trend: PriceTrend::Up, + kind: PriceModKind::Spontaneous, + }), + } + } + } + } + + fn confirm_rumors(&mut self) { + for (_, location) in &mut self.locations { + location.rumors.retain(|rumor| rumor.confirmed.is_none()); + + for rumor in &mut location.rumors { + rumor.confirmed = Some(self.rng.gen_bool(1. / 2.)); + } + } + + let all_rumors = self + .locations + .iter() + .flat_map(|(_, l)| l.rumors.clone()) + .collect::>(); + + for rumor in all_rumors { + if let Some(true) = rumor.confirmed { + let location = self.locations.get_mut(&rumor.location).unwrap(); + location.price_mods.push(PriceMod { + drug: rumor.drug.clone(), + trend: rumor.trend.clone(), + kind: PriceModKind::Rumor, + }); + } + } + } + + fn generate_rumors(&mut self) { + let locations = self.locations.keys().cloned().collect::>(); + + for (_, location) in &mut self.locations { + location.rumors.clear(); + + for (drug_name, _) in &self.drugs { + if self.rng.gen_bool(0.95) { + continue; + } + + match self.rng.gen_bool(1. / 2.) { + // Price down + true => location.rumors.push(Rumor { + drug: drug_name.clone(), + trend: PriceTrend::Down, + location: locations.choose(&mut self.rng).unwrap().clone(), + confirmed: None, + }), + // Price UP ! + false => location.rumors.push(Rumor { + drug: drug_name.clone(), + trend: PriceTrend::Up, + location: locations.choose(&mut self.rng).unwrap().clone(), + confirmed: None, + }), + } + } + } + } + + fn update_markets(&mut self) { + for (_, location) in &mut self.locations { + location.drug_market.clear(); + location.item_market.clear(); + + for (drug_name, drug) in &self.drugs { + let mods = location + .price_mods + .clone() + .into_iter() + .filter(|price_mod| *price_mod.drug == *drug_name) + .collect::>(); + + if self.rng.gen_bool(4. / 5.) && mods.len() == 0 { + continue; + }; + + let supply = self.rng.gen_range(0..1000000); + let demand = self.rng.gen_range(0..1000000); + + let deviation = demand as f64 - supply as f64; + + let ratio = 1. + (deviation / ((supply as f64).max(demand as f64) * 1.7)); + + let float_price = ((drug.nominal_price as f64) / 10000.) * ratio; + let mut price = (float_price * 10000.) as u128; + + for price_mod in mods { + match price_mod.trend { + PriceTrend::Up => price *= 15, + PriceTrend::Down => price /= 6, + } + } + + location.drug_market.insert( + drug_name.clone(), + MarketElement { + supply, + demand, + price, + }, + ); + } + + for (item_name, item) in &self.items { + if self.rng.gen_bool(4. / 5.) { + continue; + }; + + let supply = self.rng.gen_range(0..1000000); + let demand = self.rng.gen_range(0..1000000); + + let deviation = demand as f64 - supply as f64; + + let ratio = 1. + (deviation / ((supply as f64).max(demand as f64) * 1.7)); + + let float_price = ((item.nominal_price as f64) / 10000.) * ratio; + let price = (float_price * 10000.) as u128; + + location.item_market.insert( + item_name.clone(), + MarketElement { + supply, + demand, + price, + }, + ); + } + } + } + + pub fn check_new_day(&mut self, irc: &mut Irc) { + let Ok(elapsed) = self.timer.elapsed() else { return; }; + + if elapsed.as_secs_f32() > self.settings.day_duration as f32 { + let lines = self.new_day().unwrap(); + + for line in lines { + irc.privmsg_all(&line); + } + } + } + + pub fn new_day(&mut self) -> Result> { + let mut msg = PrivMsg::new(); + let msg = msg + .text("new day: ") + .color(IrcColor::LightGreen) + .text(&self.get_date()) + .get(); + + let mut lines = vec![msg.to_owned()]; + + self.laundering_fees = self.rng.gen_range((0.05)..=(0.2)); + + let mut msg = PrivMsg::new(); + let msg = msg + .text("today's laundering fees: ") + .color(IrcColor::Cyan) + .text(&format!("{:.2}%", self.laundering_fees * 100.)) + .get(); + + lines.push(msg.to_owned()); + + self.timer = SystemTime::now(); + self.settings.current_day += Duration::days(1); + + // do price mods + self.update_price_mods(); + // do rumor truthness + self.confirm_rumors(); + + for (nick, destination) in &mut self.flights.clone() { + let dealer = self.get_dealer_mut(nick).unwrap(); + + let mut msg = PrivMsg::new(); + let msg = msg + .color(IrcColor::Green) + .text("you landed at ") + .color(IrcColor::Purple) + .text(&destination) + .get(); + + lines.append(&mut hl_message(&nick, msg)); + dealer.location = destination.clone(); + dealer.status = DealerStatus::Available; + + let location = self.locations.get_mut(destination.as_str()).unwrap(); + location.blokes.insert(nick.clone()); + } + + for shipment in &mut self.shipments.clone() { + let mut msg = PrivMsg::new(); + let msg = msg + .color(IrcColor::Green) + .text("you shipment of ") + .color(IrcColor::Yellow) + .text(&format!( + "{} {}", + pretty_print_amount(shipment.amount), + shipment.element + )) + .reset() + .text(" arrived at ") + .color(IrcColor::Purple) + .text(&shipment.destination) + .get(); + + lines.append(&mut hl_message(&shipment.owner, msg)); + + let is_drug = self.get_matching::(&shipment.element).is_ok(); + + let dealer = self.get_dealer_mut(&shipment.owner).unwrap(); + + // todo: use generics insteam + if is_drug { + dealer.add_at::( + &shipment.destination, + &shipment.element, + shipment.amount, + shipment.bought_at, + ); + } else { + dealer.add_at::( + &shipment.destination, + &shipment.element, + shipment.amount, + shipment.bought_at, + ); + } + } + + let mut respawn_msg = PrivMsg::new(); + let respawn_msg = respawn_msg + .color(IrcColor::Green) + .text("You respawned!") + .get(); + + for (dealer_nick, dealer) in &mut self.dealers { + dealer.has_attacked = false; + if let DealerStatus::Dead(from) = dealer.status { + if self.settings.current_day - from == Duration::days(7) { + lines.append(&mut hl_message(dealer_nick, respawn_msg)); + dealer.health = 100.; + dealer.status = DealerStatus::Available; + dealer.looters = HashSet::default(); + } + } + } + + self.flights.clear(); + self.shipments.clear(); + + self.update_markets(); + + // make new rumors + self.generate_rumors(); + + let save = DrugWarsSave::from(self); + if let Err(e) = save.save() { + println!("{:?}", e); + }; + + Ok(lines) + } + + pub fn get_dealer(&self, nick: &str) -> Result<&Dealer> { + match self.dealers.get(nick) { + Some(dealer) => Ok(dealer), + None => Err(Error::DealerNotPlaying), + } + } + + pub fn get_dealer_available(&self, nick: &str) -> Result<&Dealer> { + let dealer = match self.dealers.get(nick) { + Some(dealer) => dealer, + None => return Err(Error::DealerNotPlaying), + }; + + if !dealer.available() { + return Err(Error::DealerNotAvailable(dealer.status.clone())); + } + + Ok(dealer) + } + + pub fn get_dealer_mut(&mut self, nick: &str) -> Result<&mut Dealer> { + match self.dealers.get_mut(nick) { + Some(dealer) => Ok(dealer), + None => Err(Error::DealerNotPlaying), + } + } + + pub fn get_matching<'a, T: Matchable>(&'a self, name: &'a str) -> Result<(&'a String, &'a T)> { + let matching_elements = T::get_matching_elements(self, &name); + + if matching_elements.len() == 0 { + return Err(Error::ElementNotFound(name.to_owned())); + } + + Ok(matching_elements[0]) + } + + pub fn get_amount_from_str(&self, amount_str: &str) -> Result + where + ::Err: Debug, + { + //TODO: let users use `max` as amount + + let Ok(amount) = amount_str.parse::() else { + return Err(Error::ParseError(amount_str.to_owned())); + }; + + if amount_str.parse::().unwrap() <= 0 { + return Err(Error::AmountIsZero); + } + + return Ok(amount); + } + + pub fn get_sell_amount_of( + &self, + dealer: &Dealer, + elem_name: &str, + amount_str: &str, + ) -> Result { + if amount_str.to_lowercase() != "max" { + let Ok(amount) = amount_str.parse::() else { + return Err(Error::ParseError(amount_str.to_owned())); + }; + + return Ok(amount); + } + + let owned_elements = dealer.get_owned_local::(); + + let market = self + .locations + .get(&dealer.location) + .unwrap() + .get_market::(); + + let Some(market_elem) = market.get(elem_name) else { + return Err(Error::ElementNotFound(elem_name.to_owned())); + }; + + let Some(owned_elem) = owned_elements.get(elem_name) else { + return Err(Error::ElementNotFound(elem_name.to_owned())); + }; + + Ok(market_elem.demand.min(owned_elem.amount)) + } + + pub fn get_buy_amount_of( + &self, + dealer: &Dealer, + elem_name: &str, + amount_str: &str, + ) -> Result { + if amount_str.to_lowercase() != "max" { + let Ok(amount) = amount_str.parse::() else { + return Err(Error::ParseError(amount_str.to_owned())); + }; + + return Ok(amount); + } + + let remaining_capacity = dealer.get_remaining_capacity_local::(); + + let location = self.locations.get(&dealer.location).unwrap(); + + let market = location.get_market::(); + + let Some(market_elem) = market.get(elem_name) else { + return Err(Error::ElementNotFound(elem_name.to_owned())); + }; + + let max_money_can_buy = dealer.money / market_elem.price; + + Ok(remaining_capacity + .min(market_elem.supply) + .min(max_money_can_buy as usize)) + } + + pub fn get_give_amount_of( + &self, + dealer: &Dealer, + target_dealer: &Dealer, + elem_name: &str, + amount_str: &str, + ) -> Result { + if amount_str.to_lowercase() != "max" { + let Ok(amount) = amount_str.parse::() else { + return Err(Error::ParseError(amount_str.to_owned())); + }; + + return Ok(amount); + } + + let target_remaining_cap = target_dealer.get_remaining_capacity_local::(); + + let owned_elements = dealer.get_owned_local::(); + let Some(owned_elem) = owned_elements.get(elem_name) else { + return Err(Error::ElementNotFound(elem_name.to_owned())); + }; + + Ok(owned_elem.amount.min(target_remaining_cap)) + } + + pub fn get_ship_amount_of( + &self, + dealer: &Dealer, + destination_name: &str, + elem_name: &str, + amount_str: &str, + ) -> Result { + if amount_str.to_lowercase() != "max" { + let Ok(amount) = amount_str.parse::() else { + return Err(Error::ParseError(amount_str.to_owned())); + }; + + return Ok(amount); + } + + let dest_cap = dealer.get_remaining_capacity_at::(destination_name); + + let owned_elements = dealer.get_owned_local::(); + let Some(owned_elem) = owned_elements.get(elem_name) else { + return Err(Error::ElementNotFound(elem_name.to_owned())); + }; + + Ok(owned_elem.amount.min(dest_cap)) + } + + pub fn get_money_amount(&self, dealer: &Dealer, amount_str: &str) -> Result { + if amount_str.to_lowercase() != "max" { + let Ok(amount) = amount_str.parse::() else { + return Err(Error::ParseError(amount_str.to_owned())); + }; + let amount = (amount * 10000.) as u128; + + if dealer.money < amount { + return Err(Error::NotEnoughMoney); + } + return Ok(amount); + } + + Ok(dealer.money) + } + + pub fn get_laundering_amount(&self, dealer: &Dealer, amount_str: &str) -> Result<(u128, u128)> { + if amount_str.to_lowercase() != "max" { + let Ok(original) = amount_str.parse::() else { + return Err(Error::ParseError(amount_str.to_owned())); + }; + + if original <= 0. { + return Err(Error::LaunderNegativeMoney); + } + + let fees = original * self.laundering_fees as f64; + + let f_amount = original - fees; + let amount = (f_amount * 10000.) as u128; + + if dealer.money < (original * 10000.) as u128 { + return Err(Error::NotEnoughMoney); + } + + return Ok((amount, (original * 10000.) as u128)); + } + + let final_amount = (dealer.money / 10000) as f64; + let unit = final_amount / ((1. + self.laundering_fees as f64) * 100.); + let original = unit * 100.; + let fees = final_amount - original; + let laundered = final_amount - fees; + + Ok(((laundered * 10000.) as u128, dealer.money)) + } + + pub fn _buy( + &mut self, + nick: &str, + name: &str, + amount: usize, + bought_at: u128, + ) -> Result> { + let dealer = self.get_dealer(nick)?; + + let location = self.locations.get_mut(&dealer.location.clone()).unwrap(); + let market = location.get_market_mut::(); + let market_element = market.get_mut(name).unwrap(); + let price = market_element.buy(amount); + + let dealer = self.get_dealer_mut(nick).unwrap(); + dealer.add_local::(name, amount, bought_at); + dealer.money -= price; + + let mut msg = PrivMsg::new(); + + let msg = msg + .text("you bought ") + .color(IrcColor::Yellow) + .text(&format!("{} {}", pretty_print_amount(amount), name)) + .reset() + .text(" for ") + .color(IrcColor::Green) + .text(&pretty_print_money(price)) + .get(); + + Ok(hl_message(nick, msg)) + } + + pub fn _sell( + &mut self, + nick: &str, + name: &str, + amount: usize, + ) -> Result> { + let dealer = self.get_dealer(nick)?; + + let location = self.locations.get_mut(&dealer.location.clone()).unwrap(); + let market = location.get_market_mut::(); + let market_element = market.get_mut(name).unwrap(); + let price = market_element.sell(amount); + + let dealer = self.get_dealer_mut(nick).unwrap(); + dealer.sub_local::(name, amount); + dealer.money += price; + + let mut msg = PrivMsg::new(); + + let msg = msg + .text("you sold ") + .color(IrcColor::Yellow) + .text(&format!("{} {}", pretty_print_amount(amount), name)) + .reset() + .text(" for ") + .color(IrcColor::Green) + .text(&pretty_print_money(price)) + .get(); + + Ok(hl_message(nick, msg)) + } + + pub fn _ship( + &mut self, + nick: &str, + name: &str, + amount: usize, + destination_str: &str, + ) -> Result> { + let dealer = self.get_dealer(nick)?; + + let elem = dealer.get_owned_local::().get(name).unwrap(); + + let shipment = Shipment { + owner: nick.to_owned(), + element: name.to_owned(), + amount, + destination: destination_str.to_owned(), + bought_at: elem.bought_at, + ..Default::default() + }; + + let dealer_location = self.locations.get(&dealer.location).unwrap(); + let destination = self.locations.get(destination_str).unwrap(); + let price = get_shipping_price(dealer_location, destination, amount); + + let dealer = self.get_dealer_mut(nick)?; + dealer.sub_local::(name, amount); + dealer.money -= price; + + self.shipments.push(shipment); + + let mut msg = PrivMsg::new(); + + let msg = msg + .text("you shipped ") + .color(IrcColor::Yellow) + .text(&format!("{} {}", pretty_print_amount(amount), name)) + .reset() + .text(" to ") + .color(IrcColor::Purple) + .text(destination_str) + .reset() + .text(" for ") + .color(IrcColor::Green) + .text(&pretty_print_money(price)) + .reset() + .text(". your shipment will arrive tomorrow.") + .get(); + + Ok(hl_message(nick, msg)) + } + + pub fn _give_money( + &mut self, + nick: &str, + bloke_nick: &str, + money: u128, + ) -> Result> { + let dealer = self.get_dealer_mut(nick).unwrap(); + dealer.money -= money; + + let bloke = self.get_dealer_mut(bloke_nick).unwrap(); + bloke.money += money; + + let mut msg = PrivMsg::new(); + + let msg = msg + .text("you gave ") + .color(IrcColor::Yellow) + .text(bloke_nick) + .text(" ") + .color(IrcColor::Green) + .text(&pretty_print_money(money)) + .get(); + + Ok(hl_message(nick, msg)) + } + + pub fn _give( + &mut self, + nick: &str, + bloke_nick: &str, + name: &str, + amount: usize, + ) -> Result> { + let dealer = self.get_dealer_mut(nick).unwrap(); + dealer.sub_local::(name, amount); + + let bloke = self.get_dealer_mut(bloke_nick).unwrap(); + bloke.add_local::(name, amount, 0); + + let mut msg = PrivMsg::new(); + + let msg = msg + .text("you gave ") + .color(IrcColor::Yellow) + .text(&format!("{} {}", pretty_print_amount(amount), name)) + .reset() + .text(" to ") + .color(IrcColor::Purple) + .text(bloke_nick) + .get(); + + Ok(hl_message(nick, msg)) + } + + pub fn _fly_to(&mut self, nick: &str, destination_name: &str) -> Result> { + let dealer = self.get_dealer(nick).unwrap(); + let current_location = self.locations.get(&dealer.location).unwrap(); + + let destination = self.locations.get(destination_name).unwrap(); + + let price = get_flight_price(current_location, destination); + + let current_location = self.locations.get_mut(&dealer.location.clone()).unwrap(); + current_location.blokes.remove(nick); + + let dealer = self.get_dealer_mut(nick).unwrap(); + + dealer.money -= price; + dealer.status = DealerStatus::Flying; + + self.flights + .insert(nick.to_string(), destination_name.to_owned()); + + let mut msg = PrivMsg::new(); + + let msg = msg + .text("you take a flight to ") + .color(IrcColor::Purple) + .text(destination_name) + .reset() + .text(" for ") + .color(IrcColor::Green) + .text(&pretty_print_money(price)) + .reset() + .text(". You'll arrive tomorrow") + .get(); + + Ok(hl_message(nick, msg)) + } + + pub fn _buy_capacity(&mut self, nick: &str, amount: usize) -> Result> { + let dealer = self.get_dealer_mut(nick).unwrap(); + + let price = capacity_price(dealer.capacity, amount)?; + + dealer.money -= price; + dealer.capacity += amount; + + let mut msg = PrivMsg::new(); + + let msg = msg + .text("you bought ") + .color(IrcColor::Yellow) + .text(&pretty_print_amount(amount)) + .reset() + .text(" slots for ") + .color(IrcColor::Green) + .text(&pretty_print_money(price)) + .get(); + + Ok(hl_message(nick, msg)) + } + + pub fn show_all_dealers(&self, nick: &str) -> Result> { + let mut lines = vec![]; + + lines.append(&mut hl_message( + nick, + &format!("There's currently {} dealers:", self.dealers.len()), + )); + + for (dealer_name, dealer) in &self.dealers { + lines.push(format!( + "{} -> money: {}, hp: {:.2}, location: {}, capacity: {}", + dealer_name, + pretty_print_money(dealer.money), + dealer.health, + dealer.location, + pretty_print_amount(dealer.capacity) + )) + } + + Ok(lines) + } + + pub fn fast_forward(&mut self) -> Result> { + self.new_day() + } + + pub fn save_game(&mut self, nick: &str) -> Result> { + let save = DrugWarsSave::from(self); + + if let Err(e) = save.save() { + return Ok(hl_error(nick, &format!("An error occured: {:?}", e))); + }; + + Ok(hl_message(nick, "game saved")) + } + + pub fn _attack( + &mut self, + nick: &str, + target_nick: &str, + weapon_str: &str, + ) -> Result> { + let dealer = self.get_dealer_mut(nick)?; + dealer.has_attacked = true; + + let (weapon_name, item) = self.get_matching::(&weapon_str)?; + let weapon_name = weapon_name.clone(); + + let ItemKind::Weapon(weapon) = &item.kind else { + return Err(Error::ItemNotWeapon(weapon_name.to_owned())); + }; + + let target_dealer = self.get_dealer(target_nick)?; + + let armor = target_dealer.get_best_armor(self); + let damage = calc_damage(&mut self.rng.clone(), weapon, &armor); + + if weapon.ammo.is_some() { + let ammo = weapon.ammo.as_ref().unwrap().clone(); + let dealer = self.get_dealer_mut(nick)?; + dealer.sub_local::(&ammo, 1); + } + + if self.rng.gen_bool(1. / 2.) { + let mut msg = PrivMsg::new(); + let msg = msg.color(IrcColor::LightBlue).text("you missed!").get(); + + return Ok(hl_message(nick, msg)); + } + + if armor.is_some() { + let target_dealer = self.get_dealer_mut(target_nick)?; + target_dealer.sub_local::(&armor.clone().unwrap().0, 1); + } + + let target_dealer = self.get_dealer_mut(target_nick)?; + + target_dealer.health -= damage; + + let mut lines = vec![]; + + let mut msg = PrivMsg::new(); + let msg = msg + .text("you hit ") + .color(IrcColor::Yellow) + .text(target_nick) + .reset() + .text(" with your ") + .color(IrcColor::Yellow) + .text(&weapon_name) + .reset() + .text(" and did ") + .color(IrcColor::LightBlue) + .text(&format!("{:.2}", damage)) + .reset() + .text(" damage!") + .get(); + + lines.append(&mut hl_message(nick, msg)); + + if self.rng.gen_bool(1. / 10.) { + let mut msg = PrivMsg::new(); + let msg = msg + .text("oh dear! You broke your ") + .color(IrcColor::Yellow) + .text(&weapon_name) + .reset() + .text("!") + .get(); + + lines.append(&mut hl_message(nick, msg)); + let dealer = self.get_dealer_mut(nick)?; + dealer.sub_local::(&weapon_name, 1); + } + + if armor.is_some() { + let mut msg = PrivMsg::new(); + let msg = msg + .text("you lost one ") + .color(IrcColor::Yellow) + .text(&armor.unwrap().0) + .get(); + lines.append(&mut hl_message(target_nick, msg)); + } + + lines.append(&mut self.death(target_nick)?); + + Ok(lines) + } + + pub fn _loot(&mut self, nick: &str, target_nick: &str) -> Result> { + let mut rng = rand::thread_rng(); + + let mut msg = PrivMsg::new(); + let msg = msg + .text("you looted ") + .color(IrcColor::Yellow) + .text(target_nick) + .reset() + .text("!") + .get(); + + let mut lines = hl_message(nick, msg); + + let dealer = self.get_dealer(nick)?; + let remaining_drug_cap = dealer.get_remaining_capacity_local::(); + let remaining_item_cap = dealer.get_remaining_capacity_local::(); + + let target_dealer = self.get_dealer_mut(target_nick)?; + target_dealer.looters.insert(nick.to_owned()); + + let money_taken = rng.gen_range(0..=target_dealer.money); + target_dealer.money -= money_taken; + + let looted_drug = target_dealer.get_random::(&mut rng); + let looted_item = target_dealer.get_random::(&mut rng); + + let mut looted_drug_amount = 0; + let mut looted_item_amount = 0; + + if let Some((looted_drug_name, looted_drug_owned)) = &looted_drug { + looted_drug_amount = + rng.gen_range(0..=looted_drug_owned.amount.min(remaining_drug_cap)); + target_dealer.sub_local::(&looted_drug_name, looted_drug_amount); + let mut msg = PrivMsg::new(); + let msg = msg + .text("you took ") + .color(IrcColor::Yellow) + .text(&format!( + "{} {}", + pretty_print_amount(looted_drug_amount), + looted_drug_name + )) + .get(); + + lines.push(msg.to_owned()); + } + + if let Some((looted_item_name, looted_item_owned)) = &looted_item { + looted_item_amount = + rng.gen_range(0..=looted_item_owned.amount.min(remaining_item_cap)); + target_dealer.sub_local::(&looted_item_name, looted_item_amount); + let mut msg = PrivMsg::new(); + let msg = msg + .text("you took ") + .color(IrcColor::Yellow) + .text(&format!( + "{} {}", + pretty_print_amount(looted_item_amount), + looted_item_name + )) + .get(); + + lines.push(msg.to_owned()); + } + + let dealer = self.get_dealer_mut(nick)?; + dealer.money += money_taken; + let mut msg = PrivMsg::new(); + let msg = msg + .text("you took ") + .color(IrcColor::Green) + .text(&pretty_print_money(money_taken)) + .get(); + + lines.push(msg.to_owned()); + + if let Some((looted_drug_name, _)) = &looted_drug { + dealer.add_local::(&looted_drug_name, looted_drug_amount, 0); + } + if let Some((looted_item_name, _)) = &looted_item { + dealer.add_local::(&looted_item_name, looted_item_amount, 0); + } + + Ok(lines) + } + + fn death(&mut self, nick: &str) -> Result> { + let death_day = self.settings.current_day.clone(); + + let dealer = self.get_dealer_mut(nick)?; + + let mut lines = vec![]; + if dealer.health <= 0. { + let mut msg = PrivMsg::new(); + let msg = msg + .text("you are ") + .color(IrcColor::Red) + .text("DEAD! ") + .reset() + .text("Anyone at ") + .color(IrcColor::Purple) + .text(&dealer.location) + .reset() + .text(" can loot you. You will be able to play again in one week.") + .get(); + + lines.append(&mut hl_message(nick, msg)); + dealer.health = 0.; + dealer.status = DealerStatus::Dead(death_day); + } + + Ok(lines) + } +} diff --git a/drugwars/src/error.rs b/drugwars/src/error.rs new file mode 100644 index 0000000..3f46c3b --- /dev/null +++ b/drugwars/src/error.rs @@ -0,0 +1,135 @@ +use crate::definitions::DealerStatus; + +pub type Error = DrugWarsError; +pub type Result = std::result::Result; + +#[derive(Debug, Clone)] +pub enum DrugWarsError { + DealerNotPlaying, + DealerNotAvailable(DealerStatus), + CantAttackDealer(String, DealerStatus), + DealerAlreadyRegistered, + InvalidSize, + NoElementAtMarket(String), + NoElementOwned(String), + NotEnoughSupply(String), + NotEnoughDemand(String), + NotEnoughElementOwned(String), + NotEnoughCapacity, + NotEnoughCapacityAt(String), + CapacityOverflow, + NotEnoughMoney, + NegativeMoney, + LaunderNegativeMoney, + BlokeNotHere(String), + BlokeNotEnoughCapacity(String), + ShipCurrentLocation, + ElementNotFound(String), + ElementAmbiguous(String), + ParseError(String), + HashMapKeyNotFound(String), + WeaponNotFound(String), + NotSameLocation(String), + ItemNotWeapon(String), + DealerNotFound(String), + AlreadyAttacked, + AlreadyFullHealth, + DealerNotDead(String), + AlreadyLooted(String), + AmountIsZero, + UnknownStatus(String), + UnknownDate(String), +} + +impl std::fmt::Display for DrugWarsError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + DrugWarsError::DealerNotPlaying => write!(f, "you aren't playing yet you donkey"), + DrugWarsError::DealerNotAvailable(status) => { + write!(f, "{}", status) + } + DrugWarsError::CantAttackDealer(nick, status) => { + write!(f, "can't attack {} -> {}", nick, status) + } + DrugWarsError::DealerAlreadyRegistered => { + write!(f, "you're already registered you bloot clot donkey") + } + DrugWarsError::InvalidSize => write!(f, "size must be between 75 and 130"), + DrugWarsError::NoElementAtMarket(name) => { + write!(f, "there isn't any {} on the market today.", name) + } + DrugWarsError::NoElementOwned(name) => { + write!(f, "you don't have any {} here.", name) + } + DrugWarsError::NotEnoughSupply(name) => { + write!(f, "there isn't enough supply of {} today.", name) + } + DrugWarsError::NotEnoughDemand(name) => { + write!(f, "there isn't enough demand of {} today.", name) + } + DrugWarsError::NotEnoughElementOwned(name) => { + write!(f, "you don't own enough {}", name) + } + DrugWarsError::NotEnoughCapacity => write!(f, "you don't have enough capacity"), + DrugWarsError::NotEnoughCapacityAt(name) => { + write!(f, "you don't have enough capacity at {}", name) + } + DrugWarsError::CapacityOverflow => { + write!(f, "you won't ever need that much capacity. will you?") + } + DrugWarsError::NotEnoughMoney => { + write!(f, "you don't have enough money you broke ass punk") + } + DrugWarsError::NegativeMoney => write!(f, "you can't give negative money"), + DrugWarsError::LaunderNegativeMoney => write!(f, "you can't launder negative money"), + DrugWarsError::BlokeNotHere(block_name) => { + write!(f, "{} isn't at your current location", block_name) + } + DrugWarsError::BlokeNotEnoughCapacity(block_name) => { + write!(f, "{} don't have enough capacity", block_name) + } + DrugWarsError::ShipCurrentLocation => { + write!(f, "you can't ship to your current location") + } + DrugWarsError::ElementNotFound(name) => write!(f, "couldn't find {}", name), + DrugWarsError::ElementAmbiguous(name) => write!(f, "{} is too ambiguous", name), + DrugWarsError::ParseError(val) => write!(f, "unable to parse '{}'", val), + DrugWarsError::HashMapKeyNotFound(key) => write!(f, "hashmap key {} not found", key), + DrugWarsError::WeaponNotFound(weapon) => { + write!(f, "you don't own any {} here.", weapon) + } + DrugWarsError::NotSameLocation(target_nick) => { + write!(f, "you are not at the same place as {}", target_nick) + } + DrugWarsError::ItemNotWeapon(weapon) => { + write!(f, "{} is not a weapon", weapon) + } + DrugWarsError::DealerNotFound(nick) => { + write!(f, "{} is not a player nick", nick) + } + DrugWarsError::AlreadyAttacked => { + write!(f, "you already attacked today") + } + DrugWarsError::AlreadyFullHealth => { + write!(f, "you already have 100 hp you donut") + } + DrugWarsError::DealerNotDead(nick) => { + write!(f, "{} isn't dead yet", nick) + } + DrugWarsError::AlreadyLooted(nick) => { + write!(f, "you already looted {}", nick) + } + DrugWarsError::AmountIsZero => { + write!(f, "can't have an amount equals zero") + } + DrugWarsError::UnknownStatus(status_str) => { + write!(f, "can't find status {}", status_str) + } + DrugWarsError::UnknownDate(date_str) => { + write!(f, "can't parse date {}", date_str) + } + } + } +} + +impl std::error::Error for DrugWarsError {} diff --git a/drugwars/src/main.rs b/drugwars/src/main.rs new file mode 100644 index 0000000..4eaa491 --- /dev/null +++ b/drugwars/src/main.rs @@ -0,0 +1,342 @@ +/// +/// TODO: fix irc colors (one bytes if followed by !number) +/// TODO: more colors +/// TODO: better save system, no need to save everything +/// TODO: panic free! +/// TODO: print rumors at destination +/// TODO: Gotta fix that bug where you can ship multiple times @ a location while exceding capacity +/// + +pub mod api; +pub mod config; +pub mod dealer; +pub mod definitions; +pub mod drug_wars; +pub mod error; +pub mod render; +pub mod renderer; +pub mod save; +pub mod utils; +pub mod admin; + +use std::{ + sync::{Arc, RwLock}, + time::Duration, +}; + +use definitions::{Drug, Item}; +use drug_wars::DrugWars; +use irc::{typemap::TypeMapKey, Irc, IrcPrefix}; +use utils::{get_system_output, DealerComponent, Matchable}; + +struct GameManager; + +impl TypeMapKey for GameManager { + type Value = Arc>; +} + +fn main() { + let drug_wars_arc = Arc::new(RwLock::new(DrugWars::load_config("drugwars_config.yaml"))); + + { + let mut drug_wars = drug_wars_arc.write().unwrap(); + drug_wars.init(); + } + + let mut irc = Irc::from_config("irc_config.yaml") + .add_resource::>, GameManager>(drug_wars_arc.clone()) + .add_default_system(melp) + .add_system("register", register) + .add_system("melp?", explodes) + .add_system("m", show_market) + .add_system("i", show_info) + .add_system("p", show_people) + .add_system("t", show_date_time) + .add_system("h", show_all_commands) + .add_system("leaderboard", leaderboard) + .add_system("l", loot) + .add_system("lm", launder) + .add_system("heal", heal) + .add_system("ha", show_admin_commands) + .add_system("gm", give_money) + .add_system("gd", give::) + .add_system("gi", give::) + .add_system("bd", buy::) + .add_system("sd", sell::) + .add_system("bi", buy::) + .add_system("bc", buy_capacity) + .add_system("si", sell::) + .add_system("cc", check_capacity_price) + .add_system("cf", check_flight_prices) + .add_system("cshd", check_shipping_prices::) + .add_system("cshi", check_shipping_prices::) + .add_system("shd", ship::) + .add_system("shi", ship::) + .add_system("f", flight) + .add_system("a", attack) + .add_admin_system("dealers", show_all_dealers) + .add_admin_system("dealer", admin_dealer) + .add_admin_system("ff", fast_forward) + .add_admin_system("save", save_game) + .build(); + + irc.connect().unwrap(); + irc.register(); + + loop { + { + let mut drug_wars = drug_wars_arc.write().unwrap(); + drug_wars.check_new_day(&mut irc); + } + irc.update(); + std::thread::sleep(Duration::from_millis(50)); + } +} + +fn register(irc: &mut Irc, prefix: &IrcPrefix, _arguments: Vec<&str>) -> Vec { + let data = irc.data().get::().unwrap(); + let mut drug_wars = data.write().unwrap(); + get_system_output(prefix.nick, drug_wars.register_dealer(prefix.nick)) +} + +fn show_market(irc: &mut Irc, prefix: &IrcPrefix, _arguments: Vec<&str>) -> Vec { + let data = irc.data().get::().unwrap(); + let drug_wars = data.read().unwrap(); + get_system_output(prefix.nick, drug_wars.show_market(prefix.nick)) +} + +fn show_info(irc: &mut Irc, prefix: &IrcPrefix, _arguments: Vec<&str>) -> Vec { + let data = irc.data().get::().unwrap(); + let drug_wars = data.read().unwrap(); + get_system_output(prefix.nick, drug_wars.show_info(prefix.nick)) +} + +fn show_people(irc: &mut Irc, prefix: &IrcPrefix, _arguments: Vec<&str>) -> Vec { + let data = irc.data().get::().unwrap(); + let drug_wars = data.read().unwrap(); + get_system_output(prefix.nick, drug_wars.show_people(prefix.nick)) +} + +fn show_date_time(irc: &mut Irc, prefix: &IrcPrefix, _arguments: Vec<&str>) -> Vec { + let data = irc.data().get::().unwrap(); + let drug_wars = data.read().unwrap(); + get_system_output(prefix.nick, drug_wars.show_date_time()) +} + +fn show_all_commands(irc: &mut Irc, prefix: &IrcPrefix, _arguments: Vec<&str>) -> Vec { + let data = irc.data().get::().unwrap(); + let drug_wars = data.read().unwrap(); + get_system_output(prefix.nick, drug_wars.show_all_commands()) +} + +fn leaderboard(irc: &mut Irc, prefix: &IrcPrefix, _arguments: Vec<&str>) -> Vec { + let data = irc.data().get::().unwrap(); + let drug_wars = data.read().unwrap(); + get_system_output(prefix.nick, drug_wars.leaderboard()) +} + +fn show_admin_commands(irc: &mut Irc, prefix: &IrcPrefix, _arguments: Vec<&str>) -> Vec { + let data = irc.data().get::().unwrap(); + let drug_wars = data.read().unwrap(); + get_system_output(prefix.nick, drug_wars.show_admin_commands()) +} + +fn melp(_irc: &mut Irc, _prefix: &IrcPrefix, _arguments: Vec<&str>) -> Vec { + vec!["melp?".to_owned()] +} + +fn explodes(_irc: &mut Irc, _prefix: &IrcPrefix, _arguments: Vec<&str>) -> Vec { + vec!["explodes.".to_owned()] +} + +fn ship( + irc: &mut Irc, + prefix: &IrcPrefix, + arguments: Vec<&str>, +) -> Vec { + let data = irc.data().get::().unwrap(); + let mut drug_wars = data.write().unwrap(); + if arguments.len() != 3 { + return get_system_output(prefix.nick, drug_wars.show_all_commands()); + } + get_system_output( + prefix.nick, + drug_wars.ship::(prefix.nick, arguments[0], arguments[1], arguments[2]), + ) +} + +fn sell( + irc: &mut Irc, + prefix: &IrcPrefix, + arguments: Vec<&str>, +) -> Vec { + let data = irc.data().get::().unwrap(); + let mut drug_wars = data.write().unwrap(); + if arguments.len() != 2 { + return get_system_output(prefix.nick, drug_wars.show_all_commands()); + } + get_system_output( + prefix.nick, + drug_wars.sell::(prefix.nick, arguments[0], arguments[1]), + ) +} + +fn buy( + irc: &mut Irc, + prefix: &IrcPrefix, + arguments: Vec<&str>, +) -> Vec { + let data = irc.data().get::().unwrap(); + let mut drug_wars = data.write().unwrap(); + if arguments.len() != 2 { + return get_system_output(prefix.nick, drug_wars.show_all_commands()); + } + get_system_output( + prefix.nick, + drug_wars.buy::(prefix.nick, arguments[0], arguments[1]), + ) +} + +fn check_flight_prices(irc: &mut Irc, prefix: &IrcPrefix, _arguments: Vec<&str>) -> Vec { + let data = irc.data().get::().unwrap(); + let drug_wars = data.write().unwrap(); + get_system_output(prefix.nick, drug_wars.check_flight_prices(prefix.nick)) +} + +fn flight(irc: &mut Irc, prefix: &IrcPrefix, arguments: Vec<&str>) -> Vec { + let data = irc.data().get::().unwrap(); + let mut drug_wars = data.write().unwrap(); + if arguments.len() != 1 { + return get_system_output(prefix.nick, drug_wars.show_all_commands()); + } + get_system_output(prefix.nick, drug_wars.fly_to(prefix.nick, arguments[0])) +} + +fn give_money(irc: &mut Irc, prefix: &IrcPrefix, arguments: Vec<&str>) -> Vec { + let data = irc.data().get::().unwrap(); + let mut drug_wars = data.write().unwrap(); + if arguments.len() != 2 { + return get_system_output(prefix.nick, drug_wars.show_all_commands()); + } + + get_system_output( + prefix.nick, + drug_wars.give_money(prefix.nick, arguments[1], arguments[0]), + ) +} + +fn give( + irc: &mut Irc, + prefix: &IrcPrefix, + arguments: Vec<&str>, +) -> Vec { + let data = irc.data().get::().unwrap(); + let mut drug_wars = data.write().unwrap(); + if arguments.len() != 3 { + return get_system_output(prefix.nick, drug_wars.show_all_commands()); + } + get_system_output( + prefix.nick, + drug_wars.give::(prefix.nick, arguments[1], arguments[2], arguments[0]), + ) +} + +fn buy_capacity(irc: &mut Irc, prefix: &IrcPrefix, arguments: Vec<&str>) -> Vec { + let data = irc.data().get::().unwrap(); + let mut drug_wars = data.write().unwrap(); + if arguments.len() != 1 { + return get_system_output(prefix.nick, drug_wars.show_all_commands()); + } + get_system_output( + prefix.nick, + drug_wars.buy_capacity(prefix.nick, arguments[0]), + ) +} + +fn check_capacity_price(irc: &mut Irc, prefix: &IrcPrefix, arguments: Vec<&str>) -> Vec { + let data = irc.data().get::().unwrap(); + let mut drug_wars = data.write().unwrap(); + if arguments.len() != 1 { + return get_system_output(prefix.nick, drug_wars.show_all_commands()); + } + get_system_output( + prefix.nick, + drug_wars.check_capacity_price(prefix.nick, arguments[0]), + ) +} + +fn check_shipping_prices( + irc: &mut Irc, + prefix: &IrcPrefix, + arguments: Vec<&str>, +) -> Vec { + let data = irc.data().get::().unwrap(); + let mut drug_wars = data.write().unwrap(); + if arguments.len() != 3 { + return get_system_output(prefix.nick, drug_wars.show_all_commands()); + } + get_system_output( + prefix.nick, + drug_wars.check_shipping_price::(prefix.nick, arguments[0], arguments[1], arguments[2]), + ) +} + +fn show_all_dealers(irc: &mut Irc, prefix: &IrcPrefix, _arguments: Vec<&str>) -> Vec { + let data = irc.data().get::().unwrap(); + let drug_wars = data.read().unwrap(); + get_system_output(prefix.nick, drug_wars.show_all_dealers(prefix.nick)) +} + +fn admin_dealer(irc: &mut Irc, prefix: &IrcPrefix, arguments: Vec<&str>) -> Vec { + let data = irc.data().get::().unwrap(); + let mut drug_wars = data.write().unwrap(); + get_system_output(prefix.nick, drug_wars.admin_dealer(prefix.nick, &arguments)) +} + +fn fast_forward(irc: &mut Irc, prefix: &IrcPrefix, _arguments: Vec<&str>) -> Vec { + let data = irc.data().get::().unwrap(); + let mut drug_wars = data.write().unwrap(); + get_system_output(prefix.nick, drug_wars.fast_forward()) +} + +fn save_game(irc: &mut Irc, prefix: &IrcPrefix, _arguments: Vec<&str>) -> Vec { + let data = irc.data().get::().unwrap(); + let mut drug_wars = data.write().unwrap(); + get_system_output(prefix.nick, drug_wars.save_game(prefix.nick)) +} + +fn attack(irc: &mut Irc, prefix: &IrcPrefix, arguments: Vec<&str>) -> Vec { + let data = irc.data().get::().unwrap(); + let mut drug_wars = data.write().unwrap(); + if arguments.len() != 2 { + return get_system_output(prefix.nick, drug_wars.show_all_commands()); + } + get_system_output( + prefix.nick, + drug_wars.attack(prefix.nick, arguments[0], arguments[1]), + ) +} + +fn loot(irc: &mut Irc, prefix: &IrcPrefix, arguments: Vec<&str>) -> Vec { + let data = irc.data().get::().unwrap(); + let mut drug_wars = data.write().unwrap(); + if arguments.len() != 1 { + return get_system_output(prefix.nick, drug_wars.show_all_commands()); + } + get_system_output(prefix.nick, drug_wars.loot(prefix.nick, arguments[0])) +} + +fn heal(irc: &mut Irc, prefix: &IrcPrefix, _arguments: Vec<&str>) -> Vec { + let data = irc.data().get::().unwrap(); + let mut drug_wars = data.write().unwrap(); + get_system_output(prefix.nick, drug_wars.heal(prefix.nick)) +} + +fn launder(irc: &mut Irc, prefix: &IrcPrefix, arguments: Vec<&str>) -> Vec { + let data = irc.data().get::().unwrap(); + let mut drug_wars = data.write().unwrap(); + if arguments.len() != 1 { + return get_system_output(prefix.nick, drug_wars.show_all_commands()); + } + get_system_output(prefix.nick, drug_wars.launder(prefix.nick, arguments[0])) +} diff --git a/drugwars/src/render.rs b/drugwars/src/render.rs new file mode 100644 index 0000000..6fe9a5f --- /dev/null +++ b/drugwars/src/render.rs @@ -0,0 +1,690 @@ +use chrono::Duration; +use irc::{format::IrcColor, privmsg::PrivMsg}; +use itertools::Itertools; +use rand::seq::SliceRandom; + +use crate::{ + dealer::Dealer, + definitions::{Drug, Item, MessageKind, PriceTrend}, + drug_wars::DrugWars, + renderer::{RenderBox, RenderBoxContent, Renderer}, + utils::{get_flight_price, pretty_print_amount, pretty_print_money, IrcSafeLen}, +}; + +impl DrugWars { + pub fn get_date_and_time(&self) -> String { + let t = self.timer.elapsed().unwrap().as_secs_f32() / self.settings.day_duration as f32; + + let current_seconds = t * 86400.; + + let duration = Duration::seconds(current_seconds as i64); + + let current_time = format!( + "{:0>2}:{:0>2}", + duration.num_hours(), + duration.num_minutes() - (60 * duration.num_hours()) + ); + + format!("{} {}", self.get_date(), current_time) + } + + pub fn get_date(&self) -> String { + let current_date = self.settings.current_day.format("%Y-%m-%d").to_string(); + current_date + } + + pub fn render_info(&self, nick: &str, dealer: &Dealer) -> Vec { + Renderer::new(50) + .add_box( + &RenderBox::new() + .headers(["Dealer Info".to_owned()]) + .add_content([&RenderBoxContent::new() + .sizes([18, 25]) + .add_row(["nick".to_owned(), nick.to_owned()]) + .add_row(["health".to_owned(), format!("{:.2} hp", dealer.health)]) + .add_row(["dirty money".to_owned(), pretty_print_money(dealer.money)]) + .add_row([ + "money laundered".to_owned(), + pretty_print_money(dealer.laundered_money), + ]) + .add_row(["location".to_owned(), dealer.location.clone()]) + .add_row(["capacity".to_owned(), pretty_print_amount(dealer.capacity)]) + .add_row(["status".to_owned(), dealer.print_status().to_owned()]) + .get()]) + .get(), + ) + .build() + } + + pub fn render_time(&self) -> Vec { + vec![self.get_date_and_time()] + } + + pub fn render_market(&self, nick: &str, dealer: &Dealer) -> Vec { + let mut renderer = Renderer::new(self.settings.width); + + let mut rng = self.rng.clone(); + + let location = self.locations.get(&dealer.location).unwrap(); + let drugs_owned = dealer.get_owned_local::(); + let items_owned = dealer.get_owned_local::(); + + let mut rumor_content = RenderBoxContent::new(); + for rumor in &location.rumors { + if rumor.confirmed.is_none() { + let mut msg = PrivMsg::new(); + let msg = msg + .color(IrcColor::Cyan) + .text("You hear a rumor that ") + .color(IrcColor::Yellow) + .text(&rumor.drug) + .color(IrcColor::Cyan); + let msg = match rumor.trend { + PriceTrend::Up => msg.text(" will be abundant in "), + PriceTrend::Down => msg.text(" will be scarce in "), + }; + + let msg = msg + .color(IrcColor::Purple) + .text(&rumor.location) + .color(IrcColor::Cyan) + .text(" tomorrow.") + .get(); + + rumor_content.add_row([msg.to_owned()]); + } + } + + for price_mod in &location.price_mods { + match price_mod.trend { + PriceTrend::Up => { + let mut message = self + .messages + .get(&MessageKind::PriceUp) + .unwrap() + .choose(&mut rng) + .unwrap() + .to_owned() + + " " + + self + .messages + .get(&MessageKind::PriceUpEnd) + .unwrap() + .choose(&mut rng) + .unwrap() + .as_str(); + + let mut privmsg = PrivMsg::new(); + let colored_drug = privmsg + .color(IrcColor::Yellow) + .text(&price_mod.drug) + .color(IrcColor::Green) + .get(); + + message = message.replace("%DRUG", colored_drug); + + let mut msg = PrivMsg::new(); + let msg = msg.color(IrcColor::Green).text(&message).reset().get(); + rumor_content.add_row([msg.to_owned()]); + } + + PriceTrend::Down => { + let mut message = self + .messages + .get(&MessageKind::PriceDown) + .unwrap() + .choose(&mut rng) + .unwrap() + .to_owned() + + " " + + self + .messages + .get(&MessageKind::PriceDownEnd) + .unwrap() + .choose(&mut rng) + .unwrap() + .as_str(); + + let mut privmsg = PrivMsg::new(); + let colored_drug = privmsg + .color(IrcColor::Yellow) + .text(&price_mod.drug) + .color(IrcColor::Orange) + .get(); + + message = message.replace("%DRUG", colored_drug); + + let mut msg = PrivMsg::new(); + let msg = msg.color(IrcColor::Orange).text(&message).reset().get(); + rumor_content.add_row([msg.to_owned()]); + } + }; + } + let rumor_content = rumor_content.get(); + + let mut drugs_market_content = RenderBoxContent::new(); + drugs_market_content + .header([ + "Drug".to_owned(), + "Supply".to_owned(), + "Demand".to_owned(), + "Price".to_owned(), + ]) + .sizes([18, 10, 10, 19]); + + let mut drugs_owned_content = RenderBoxContent::new(); + drugs_owned_content + .header([ + "Drug".to_owned(), + "Amount".to_owned(), + "Bought at".to_owned(), + ]) + .sizes([18, 10, 25]); + + for pair in location.drug_market.iter().zip_longest(drugs_owned.iter()) { + match pair { + itertools::EitherOrBoth::Both(market, owned) => { + let drug = self.drugs.get(market.0).unwrap(); + + let market_drug_name = match drugs_owned.contains_key(market.0) { + true => PrivMsg::new() + .color(IrcColor::Cyan) + .text(market.0) + .reset() + .get() + .to_owned(), + false => market.0.to_owned(), + }; + + let owned_drug_name = match location.drug_market.contains_key(owned.0) { + true => PrivMsg::new() + .color(IrcColor::Cyan) + .text(owned.0) + .reset() + .get() + .to_owned(), + false => owned.0.to_owned(), + }; + + let mut msg = PrivMsg::new(); + if market.1.price >= drug.nominal_price { + msg.color(IrcColor::Green) + .text("↗ ") + .text(&pretty_print_money(market.1.price)); + } else { + msg.color(IrcColor::Red) + .text("↘ ") + .text(&pretty_print_money(market.1.price)); + } + msg.reset(); + + drugs_market_content.add_row([ + market_drug_name, + pretty_print_amount(market.1.supply), + pretty_print_amount(market.1.demand), + msg.get().to_owned(), + ]); + + drugs_owned_content.add_row([ + owned_drug_name, + pretty_print_amount(owned.1.amount), + pretty_print_money(owned.1.bought_at), + ]); + } + itertools::EitherOrBoth::Left(market) => { + let drug = self.drugs.get(market.0).unwrap(); + + let market_drug_name = match drugs_owned.contains_key(market.0) { + true => PrivMsg::new() + .color(IrcColor::Cyan) + .text(market.0) + .reset() + .get() + .to_owned(), + false => market.0.to_owned(), + }; + + let mut msg = PrivMsg::new(); + if market.1.price >= drug.nominal_price { + msg.color(IrcColor::Green) + .text("↗ ") + .text(&pretty_print_money(market.1.price)); + } else { + msg.color(IrcColor::Red) + .text("↘ ") + .text(&pretty_print_money(market.1.price)); + } + msg.reset(); + + drugs_market_content.add_row([ + market_drug_name, + pretty_print_amount(market.1.supply), + pretty_print_amount(market.1.demand), + msg.get().to_owned(), + ]); + } + itertools::EitherOrBoth::Right(owned) => { + let owned_drug_name = match location.drug_market.contains_key(owned.0) { + true => PrivMsg::new() + .color(IrcColor::Cyan) + .text(owned.0) + .reset() + .get() + .to_owned(), + false => owned.0.to_owned(), + }; + + drugs_owned_content.add_row([ + owned_drug_name, + pretty_print_amount(owned.1.amount), + pretty_print_money(owned.1.bought_at), + ]); + } + } + } + let drugs_market_content = drugs_market_content.get(); + let drugs_owned_content = drugs_owned_content.get(); + + let mut items_market_content = RenderBoxContent::new(); + items_market_content + .header([ + "Item".to_owned(), + "Supply".to_owned(), + "Demand".to_owned(), + "Price".to_owned(), + ]) + .sizes([18, 10, 10, 19]); + + let mut items_owned_content = RenderBoxContent::new(); + items_owned_content + .header([ + "Item".to_owned(), + "Amount".to_owned(), + "Bought at".to_owned(), + ]) + .sizes([18, 10, 25]); + + for pair in location.item_market.iter().zip_longest(items_owned.iter()) { + match pair { + itertools::EitherOrBoth::Both(market, owned) => { + let item = self.items.get(market.0).unwrap(); + + let market_item_name = match items_owned.contains_key(market.0) { + true => PrivMsg::new() + .color(IrcColor::Cyan) + .text(market.0) + .reset() + .get() + .to_owned(), + false => market.0.to_owned(), + }; + + let owned_item_name = match location.item_market.contains_key(owned.0) { + true => PrivMsg::new() + .color(IrcColor::Cyan) + .text(owned.0) + .reset() + .get() + .to_owned(), + false => owned.0.to_owned(), + }; + + let mut msg = PrivMsg::new(); + if market.1.price >= item.nominal_price { + msg.color(IrcColor::Green) + .text("↗ ") + .text(&pretty_print_money(market.1.price)); + } else { + msg.color(IrcColor::Red) + .text("↘ ") + .text(&pretty_print_money(market.1.price)); + } + msg.reset(); + + items_market_content.add_row([ + market_item_name, + pretty_print_amount(market.1.supply), + pretty_print_amount(market.1.demand), + msg.get().to_owned(), + ]); + + items_owned_content.add_row([ + owned_item_name, + pretty_print_amount(owned.1.amount), + pretty_print_money(owned.1.bought_at), + ]); + } + itertools::EitherOrBoth::Left(market) => { + let item = self.items.get(market.0).unwrap(); + + let market_item_name = match items_owned.contains_key(market.0) { + true => PrivMsg::new() + .color(IrcColor::Cyan) + .text(market.0) + .reset() + .get() + .to_owned(), + false => market.0.to_owned(), + }; + + let mut msg = PrivMsg::new(); + if market.1.price >= item.nominal_price { + msg.color(IrcColor::Green) + .text("↗ ") + .text(&pretty_print_money(market.1.price)); + } else { + msg.color(IrcColor::Red) + .text("↘ ") + .text(&pretty_print_money(market.1.price)); + } + msg.reset(); + + items_market_content.add_row([ + market_item_name, + pretty_print_amount(market.1.supply), + pretty_print_amount(market.1.demand), + msg.get().to_owned(), + ]); + } + itertools::EitherOrBoth::Right(owned) => { + let owned_item_name = match location.item_market.contains_key(owned.0) { + true => PrivMsg::new() + .color(IrcColor::Cyan) + .text(owned.0) + .reset() + .get() + .to_owned(), + false => owned.0.to_owned(), + }; + + items_owned_content.add_row([ + owned_item_name, + pretty_print_amount(owned.1.amount), + pretty_print_money(owned.1.bought_at), + ]); + } + } + } + let items_market_content = items_market_content.get(); + let items_owned_content = items_owned_content.get(); + + let rumor_box = RenderBox::new() + .headers([format!( + "{} ─ {} ─ {} ─ {} ─ {}", + nick, + format!("{:.2} hp", dealer.health), + pretty_print_money(dealer.money), + dealer.location, + dealer.print_status() + )]) + .add_content([&rumor_content]) + .get(); + + let drugs_box = RenderBox::new() + .headers([ + "Drug market".to_owned(), + format!( + "Owned drugs ({}/{})", + pretty_print_amount(dealer.get_total_owned_local::()), + pretty_print_amount(dealer.capacity), + ), + ]) + .add_content([&drugs_market_content, &drugs_owned_content]) + .get(); + + let items_box = RenderBox::new() + .headers([ + "Item market".to_owned(), + format!( + "Owned items ({}/{})", + pretty_print_amount(dealer.get_total_owned_local::()), + pretty_print_amount(dealer.capacity), + ), + ]) + .add_content([&items_market_content, &items_owned_content]) + .get(); + + renderer + .add_box(&rumor_box) + .add_box(&drugs_box) + .add_box(&items_box); + + renderer.build() + } + + pub fn render_people(&self, dealer: &Dealer) -> Vec { + let location = self.locations.get(&dealer.location).unwrap(); + let mut blokes = location.blokes.iter().collect::>(); + let mut line = String::new(); + + let mut blokes_content = RenderBoxContent::new(); + + while blokes.len() > 0 { + let to_append = format!("{}, ", blokes[blokes.len() - 1]); + + if line.irc_safe_len() + to_append.irc_safe_len() > self.settings.width - 2 { + line.truncate(line.len() - 2); + blokes_content.add_row([line]); + + line = String::new(); + } + + line += &to_append; + blokes.pop(); + } + + if line.irc_safe_len() > 0 { + line.truncate(line.len() - 2); + blokes_content.add_row([line]); + } + let blokes_content = blokes_content.get(); + + Renderer::new(self.settings.width) + .add_box( + &RenderBox::new() + .headers(["People in town".to_owned()]) + .add_content([&blokes_content]) + .get(), + ) + .build() + } + + pub fn render_command_list(&self) -> Vec { + Renderer::new(90) + .add_box( + &RenderBox::new() + .headers(["Command list".to_owned()]) + .add_content([&RenderBoxContent::new() + .add_row(["register".to_owned(), "join the game".to_owned()]) + .add_row(["h".to_owned(), "print this list".to_owned()]) + .add_row(["ha".to_owned(), "print the admin command list".to_owned()]) + .add_row(["i".to_owned(), "print your info".to_owned()]) + .add_row(["m".to_owned(), "print the market".to_owned()]) + .add_row([ + "p".to_owned(), + "show the people at your location".to_owned(), + ]) + .add_row(["t".to_owned(), "print the date and time".to_owned()]) + .add_row([ + "a ".to_owned(), + "attack someone".to_owned(), + ]) + .add_row(["l ".to_owned(), "loot a dead player".to_owned()]) + .add_row(["lm ".to_owned(), "launder your money".to_owned()]) + .add_row([ + "leaderboard".to_owned(), + "show the hardest dealers".to_owned(), + ]) + .add_row([ + "heal".to_owned(), + "heal completely for a third of your money".to_owned(), + ]) + .add_row([ + "bt ".to_owned(), + "buy thugs (cost 10,000 / day)".to_owned(), + ]) + .add_row(["st ".to_owned(), "sell thugs".to_owned()]) + .add_row([ + "bd ".to_owned(), + "buy drug from market".to_owned(), + ]) + .add_row([ + "sd ".to_owned(), + "sell drug to market".to_owned(), + ]) + .add_row([ + "bi ".to_owned(), + "buy item from market".to_owned(), + ]) + .add_row([ + "si ".to_owned(), + "sell item to market".to_owned(), + ]) + .add_row(["bc ".to_owned(), "buy inventory slots".to_owned()]) + .add_row([ + "cc ".to_owned(), + "check price to add inventory slots".to_owned(), + ]) + .add_row(["cf ".to_owned(), "check flight prices".to_owned()]) + .add_row([ + "f ".to_owned(), + "fly to destination".to_owned(), + ]) + .add_row([ + "cshd ".to_owned(), + "check drug shipping price".to_owned(), + ]) + .add_row([ + "cshi ".to_owned(), + "check item shipping price".to_owned(), + ]) + .add_row([ + "shd ".to_owned(), + "ship drug to destination".to_owned(), + ]) + .add_row([ + "shi ".to_owned(), + "ship item to destination".to_owned(), + ]) + .add_row([ + "gm ".to_owned(), + "give money to some bloke".to_owned(), + ]) + .add_row([ + "gd ".to_owned(), + "give drugs to some bloke".to_owned(), + ]) + .add_row([ + "gi ".to_owned(), + "give items to some bloke".to_owned(), + ]) + .get()]) + .get(), + ) + .build() + } + + pub fn render_admin_command_list(&self) -> Vec { + Renderer::new(90) + .add_box( + &RenderBox::new() + .headers(["Command list".to_owned()]) + .add_content([&RenderBoxContent::new() + .add_row(["save".to_owned(), "save the game".to_owned()]) + .add_row(["dealers".to_owned(), "show all dealers".to_owned()]) + .add_row(["ff".to_owned(), "advance to next day".to_owned()]) + .get()]) + .get(), + ) + .build() + } + + pub fn render_prices_from(&self, location_str: &str) -> Vec { + let current_location = self.locations.get(location_str).unwrap(); + + let mut flight_prices_content = RenderBoxContent::new(); + + flight_prices_content + .header(["To".to_owned(), "Price".to_owned()]) + .sizes([30, 15]); + + for (location_name, location) in &self.locations { + if location_name.as_str() == location_str { + continue; + } + + let price = get_flight_price(current_location, location); + + let to = PrivMsg::new() + .color(IrcColor::Yellow) + .text(location_name) + .reset() + .get() + .to_owned(); + let p_price = PrivMsg::new() + .color(IrcColor::Green) + .text(&pretty_print_money(price)) + .reset() + .get() + .to_owned(); + + flight_prices_content.add_row([to, p_price]); + } + + Renderer::new(50) + .add_box( + &RenderBox::new() + .headers([format!("Flight prices from {}", location_str)]) + .add_content([&flight_prices_content.get()]) + .get(), + ) + .build() + } + + pub fn render_leaderboard(&self) -> Vec { + let dealers = &self + .dealers + .iter() + .sorted_by_key(|(_, k)| k.laundered_money) + .rev() + .enumerate() + .collect::>(); + + let min = dealers.len().min(5); + + let dealers = &dealers[0..min]; + + let mut leaderboard_content = RenderBoxContent::new(); + leaderboard_content + .header([ + "Dealer".to_owned(), + "Place".to_owned(), + "Laundered money".to_owned(), + ]) + .sizes([12, 8, 25]); + + for (idx, (name, dealer)) in dealers { + let mut msg = PrivMsg::new(); + let msg = msg + .color(IrcColor::Green) + .text(&pretty_print_money(dealer.laundered_money)) + .reset() + .get(); + + leaderboard_content.add_row([ + name.to_owned().clone(), + (idx + 1).to_string(), + msg.to_owned(), + ]); + } + + Renderer::new(50) + .add_box( + &RenderBox::new() + .headers(["Top 5 hardest dealers".to_owned()]) + .add_content([&leaderboard_content]) + .get(), + ) + .build() + } +} diff --git a/drugwars/src/renderer.rs b/drugwars/src/renderer.rs new file mode 100644 index 0000000..9934bd8 --- /dev/null +++ b/drugwars/src/renderer.rs @@ -0,0 +1,255 @@ +use crate::utils::{truncate_string, IrcSafeLen}; + +pub trait BoxContent { + fn get_lines(&self, width: usize) -> Vec; + fn len(&self) -> usize; +} + +pub trait Part { + fn get_lines(&self, width: usize) -> Vec; +} + +#[derive(Default, Clone)] +pub struct RenderBoxContent { + header: Option<[String; N]>, + content: Vec<[String; N]>, + sizes: Option<[usize; N]>, +} + +impl RenderBoxContent { + pub fn new() -> Self { + Self::default() + } + + pub fn header(&mut self, header: [String; N]) -> &mut Self { + self.header = Some(header); + self + } + + pub fn sizes(&mut self, sizes: [usize; N]) -> &mut Self { + self.sizes = Some(sizes); + self + } + + pub fn add_row(&mut self, row: [String; N]) -> &mut Self { + self.content.push(row); + self + } + pub fn get(&self) -> Self { + self.clone() + } +} + +impl BoxContent for RenderBoxContent { + fn get_lines(&self, width: usize) -> Vec { + //let column_width = width / N; + + let mut lines = vec![]; + + if let Some(header) = &self.header { + let mut header_line = String::new(); + + for (idx, cell) in header.iter().enumerate() { + let column_width = match self.sizes { + Some(sizes) => sizes[idx], + None => width / N, + }; + + let value = truncate_string(&cell, column_width); + let spaces = " ".repeat(column_width - value.len()); + header_line += &format!("{}{}", value, spaces); + } + header_line += &" ".repeat(width - header_line.len()); + lines.push(header_line); + } + + for row in &self.content { + let mut row_line = String::new(); + + for (idx, cell) in row.iter().enumerate() { + let column_width = match self.sizes { + Some(sizes) => sizes[idx], + None => width / N, + }; + + let value = truncate_string(&cell, column_width); + let spaces = " ".repeat(column_width - value.irc_safe_len()); + row_line += &format!("{}{}", value, spaces); + } + + row_line += &" ".repeat(width - row_line.irc_safe_len()); + lines.push(row_line); + } + lines + } + + fn len(&self) -> usize { + let mut len = self.content.len(); + + if self.header.is_some() { + len += 1; + } + len + } +} + +#[derive(Default, Clone)] +pub struct RenderBox<'a, const N: usize> { + headers: Option<[String; N]>, + columns: Option<[&'a dyn BoxContent; N]>, + sizes: Option<[usize; N]>, +} + +impl<'a, const N: usize> RenderBox<'a, N> { + pub fn new() -> Self { + Self::default() + } + + pub fn headers(&mut self, headers: [String; N]) -> &mut Self { + self.headers = Some(headers); + self + } + + pub fn sizes(&mut self, sizes: [usize; N]) -> &mut Self { + self.sizes = Some(sizes); + self + } + + pub fn add_content(&mut self, columns: [&'a dyn BoxContent; N]) -> &mut Self { + self.columns = Some(columns); + self + } + + pub fn get(&self) -> Self { + self.clone() + } +} + +impl<'a, const N: usize> Part for RenderBox<'a, N> { + fn get_lines(&self, width: usize) -> Vec { + let column_width = (width / N) - 3; + + let max_rows = self + .columns + .unwrap() + .iter() + .fold(0, |acc, elem| acc.max(elem.len())); + + let column_lines = self + .columns + .unwrap() + .iter() + .map(|column| column.get_lines(column_width)) + .collect::>(); + + let mut lines = vec![]; + + match &self.headers { + Some(column_headers) => { + let mut line = String::new(); + + let mut first = true; + for header in column_headers { + if first { + line += "╭"; + first = false; + } else { + line += "┬"; + } + let value = format!("{} ", truncate_string(&header, column_width)); + line += &format!(" {} {}", value, "─".repeat(column_width - value.len())); + } + + if line.irc_safe_len() == width { + line.pop(); + } else if line.irc_safe_len() < width - 1 { + line += &"─".repeat(width - line.irc_safe_len() - 1); + } + + line += "╮"; + lines.push(line); + } + None => { + lines.push(format!("╭{}╮", "─".repeat(width - 2))); + } + } + + if max_rows > 0 { + for row_index in 0..max_rows { + let mut line = String::new(); + + for col_index in 0..N { + if row_index >= column_lines[col_index].len() { + line += &format!("│ {} ", " ".repeat(column_width)); + } else { + let column_line = &column_lines[col_index][row_index]; + line += &format!("│ {} ", column_line); + } + } + + if line.irc_safe_len() == width { + line.pop(); + } else if line.irc_safe_len() < width - 1 { + line += &" ".repeat(width - line.irc_safe_len() - 1); + } + + line += "│"; + + lines.push(line); + } + } + + let mut bottom_line = String::new(); + + let mut first = true; + for _ in 0..N { + if first { + bottom_line += "╰"; + first = false; + } else { + bottom_line += "┴"; + } + bottom_line += &format!("{}", "─".repeat(width / N - 1)); + } + + if bottom_line.irc_safe_len() == width { + bottom_line.pop(); + } else if bottom_line.irc_safe_len() < width - 1 { + bottom_line += &"─".repeat(width - bottom_line.irc_safe_len() - 1); + } + + bottom_line += "╯"; + lines.push(bottom_line); + + lines + } +} +pub struct Renderer<'a> { + width: usize, + boxes: Vec<&'a dyn Part>, +} + +impl<'a> Renderer<'a> { + pub fn new(width: usize) -> Self { + Self { + width: width, + boxes: vec![], + } + } + + pub fn add_box(&mut self, element: &'a impl Part) -> &mut Self { + self.boxes.push(element); + self + } + + pub fn build(&self) -> Vec { + let out = self + .boxes + .iter() + .map(|elem| elem.get_lines(self.width)) + .flatten() + .collect::>(); + + out + } +} diff --git a/drugwars/src/save.rs b/drugwars/src/save.rs new file mode 100644 index 0000000..36a83d0 --- /dev/null +++ b/drugwars/src/save.rs @@ -0,0 +1,199 @@ +use std::{ + collections::HashMap, + fs::File, + io::{Read, Write}, + str::FromStr, + time::SystemTime, +}; + +use chrono::NaiveDate; +use rand::{rngs::StdRng, SeedableRng}; +use serde::{Deserialize, Serialize}; + +use crate::{ + config::DrugWarsConfig, + dealer::Dealer, + definitions::{Armor, Drug, Item, ItemKind, Location, MessageKind, Settings, Shipment, Weapon}, + drug_wars::DrugWars, +}; + +#[derive(Serialize, Deserialize)] +pub struct DrugWarsSave { + pub day_duration: u32, + pub current_day: String, + pub save_path: String, + pub config_path: String, + pub width: usize, + pub timer: SystemTime, + pub dealers: HashMap, + pub locations: HashMap, + pub flights: HashMap, + pub shipments: Vec, + pub laundering_fees: f32, +} + +impl From<&mut DrugWars> for DrugWarsSave { + fn from(drug_wars: &mut DrugWars) -> Self { + Self { + day_duration: drug_wars.settings.day_duration, + current_day: drug_wars + .settings + .current_day + .format("%Y-%m-%d") + .to_string(), + save_path: drug_wars.settings.save_path.clone(), + config_path: drug_wars.settings.config_path.clone(), + width: drug_wars.settings.width, + timer: drug_wars.timer, + dealers: drug_wars.dealers.clone(), + locations: drug_wars.locations.clone(), + flights: drug_wars.flights.clone(), + shipments: drug_wars.shipments.clone(), + laundering_fees: drug_wars.laundering_fees, + } + } +} + +impl Into for DrugWarsSave { + fn into(self) -> DrugWars { + let config: DrugWarsConfig = DrugWarsConfig::from_file(&self.config_path).unwrap(); + + let mut drugs = HashMap::default(); + let mut items = HashMap::default(); + let mut messages = HashMap::default(); + + for drug in config.drugs { + let name = drug.as_mapping().unwrap()["name"].as_str().unwrap(); + let price = drug.as_mapping().unwrap()["price"].as_f64().unwrap(); + drugs.insert( + name.to_owned(), + Drug { + nominal_price: (price * 10000.) as u128, + }, + ); + } + + let weapons = config.items["weapons"] + .as_sequence() + .unwrap() + .iter() + .map(|value| value.as_mapping().unwrap()) + .collect::>(); + + for weapon in weapons { + let name = weapon["name"].as_str().unwrap(); + let price = weapon["price"].as_f64().unwrap(); + let damage = weapon["damage"].as_f64().unwrap() as f32; + + let mut ammo = None; + + if weapon.contains_key("ammo") { + ammo = Some(weapon["ammo"].as_str().unwrap().to_owned()) + } + + items.insert( + name.to_owned(), + Item { + nominal_price: (price * 10000.) as u128, + kind: ItemKind::Weapon(Weapon { ammo, damage }), + }, + ); + } + + let ammos = config.items["ammos"] + .as_sequence() + .unwrap() + .iter() + .map(|value| value.as_mapping().unwrap()) + .collect::>(); + + for ammo in ammos { + let name = ammo["name"].as_str().unwrap(); + let price = ammo["price"].as_f64().unwrap(); + + items.insert( + name.to_owned(), + Item { + nominal_price: (price * 10000.) as u128, + kind: ItemKind::Ammo, + }, + ); + } + + let armors = config.items["armors"] + .as_sequence() + .unwrap() + .iter() + .map(|value| value.as_mapping().unwrap()) + .collect::>(); + + for armor in armors { + let name = armor["name"].as_str().unwrap(); + let price = armor["price"].as_f64().unwrap(); + let block = armor["block"].as_f64().unwrap() as f32; + + items.insert( + name.to_owned(), + Item { + nominal_price: (price * 10000.) as u128, + kind: ItemKind::Armor(Armor { block }), + }, + ); + } + + // OH LOOK ! I'M FUCKING SLEEP DEPRIVATED ! + for (val_str, enum_variant) in [ + ("price_up", MessageKind::PriceUp), + ("price_up_end", MessageKind::PriceUpEnd), + ("price_down", MessageKind::PriceDown), + ("price_down_end", MessageKind::PriceDownEnd), + ] { + let msgs = &config.messages[val_str].as_sequence().unwrap(); + for msg in *msgs { + let message_vec = messages.entry(enum_variant).or_insert_with(|| vec![]); + message_vec.push(msg.as_str().unwrap().to_owned()); + } + } + + DrugWars { + settings: Settings { + day_duration: self.day_duration, + current_day: NaiveDate::from_str(&self.current_day).unwrap(), + save_path: self.save_path, + config_path: self.config_path, + width: self.width, + }, + rng: StdRng::from_entropy(), + timer: self.timer, + dealers: self.dealers.clone(), + locations: self.locations.clone(), + flights: self.flights.clone(), + shipments: self.shipments.clone(), + drugs, + items, + messages, + laundering_fees: self.laundering_fees, + } + } +} + +impl DrugWarsSave { + pub fn save(&self) -> std::io::Result<()> { + let content_str = serde_yaml::to_string(self).unwrap(); + let mut content = content_str.as_bytes(); + + let mut file = File::create(&self.save_path)?; + + file.write_all(&mut content)?; + Ok(()) + } + + pub fn load(path: &str) -> std::io::Result { + let mut file = File::open(path)?; + let mut contents = String::new(); + file.read_to_string(&mut contents)?; + + let config: DrugWarsSave = serde_yaml::from_str(&contents).unwrap(); + Ok(config) + } +} diff --git a/drugwars/src/utils.rs b/drugwars/src/utils.rs new file mode 100644 index 0000000..fbebd88 --- /dev/null +++ b/drugwars/src/utils.rs @@ -0,0 +1,227 @@ +use std::{collections::HashMap, f32::consts::PI, str}; + +use rand::{Rng, RngCore}; + +use crate::{ + dealer::Dealer, + definitions::{Armor, Location, MarketElement, OwnedElement, Weapon}, + drug_wars::DrugWars, + error::Result, +}; + +pub trait Matchable { + fn get_matching_elements<'a>( + drug_wars: &'a DrugWars, + name: &'a str, + ) -> Vec<(&'a String, &'a Self)> + where + Self: Sized; +} + +pub trait DealerComponent { + fn get_elements_at<'a>( + dealer: &'a Dealer, + location: &'a str, + ) -> &'a HashMap; + + fn get_elements_at_mut<'a>( + dealer: &'a mut Dealer, + location: &'a str, + ) -> &'a mut HashMap; + + fn get_market_at<'a>(location: &'a Location) -> &'a HashMap; + fn get_market_at_mut<'a>(location: &'a mut Location) -> &'a mut HashMap; +} + +pub fn truncate_string(original: &str, max: usize) -> String { + assert!(max > 3); + + if original.irc_safe_len() <= max { + return original.to_owned(); + } + + format!("{}...", &original[..(max - 3)]) +} + +pub fn pretty_print_amount(amount: usize) -> String { + let pretty_amount = amount + .to_string() + .as_bytes() + .rchunks(3) + .rev() + .map(str::from_utf8) + .collect::, _>>() + .unwrap() + .join(","); + + format!("{}", pretty_amount) +} + +pub fn pretty_print_money(money: u128) -> String { + let unit_money = money / 10000; + let float_money = money as f64 / 10000.; + let dec = (float_money.fract() * 100.).floor() as u32; + + let pretty_money = unit_money + .to_string() + .as_bytes() + .rchunks(3) + .rev() + .map(str::from_utf8) + .collect::, _>>() + .unwrap() + .join(","); + + format!("${}.{:0>2}", pretty_money, dec) +} + +pub fn get_flight_price(origin: &Location, other: &Location) -> u128 { + let cur_lat = origin.lat * (PI / 180.); + let cur_long = origin.long * (PI / 180.); + + let other_lat = other.lat * (PI / 180.); + let other_long = other.long * (PI / 180.); + + let float_price = (cur_lat.sin() * other_lat.sin() + + cur_lat.cos() * other_lat.cos() * (other_long - cur_long).cos()) + .acos() + * 10000.; + + (float_price * 10000.) as u128 +} + +pub fn get_shipping_price(origin: &Location, other: &Location, amount: usize) -> u128 { + let flight_price = get_flight_price(origin, other); + + let shipping_price = (flight_price / 80) * amount as u128; + + shipping_price +} + +pub fn capacity_price(current_capacity: usize, to_add: usize) -> Result { + let price: f64 = 1000. * 10000.; + + let paid = price + current_capacity as f64 * (20000000. * current_capacity as f64 / 1000.); + let total = current_capacity as f64 + to_add as f64; + let max = price + total * (20000000. * total as f64 / 1000.); + let to_be_paid = max - paid; + + Ok(to_be_paid as u128) +} + +pub fn hl_message(nick: &str, message: &str) -> Vec { + return vec![format!("{}: {}", nick, message)]; +} + +pub fn hl_error(nick: &str, message: &str) -> Vec { + return vec![format!("{}: {}", nick, message)]; +} + +pub fn get_system_output(nick: &str, val: Result>) -> Vec { + match val { + Ok(output) => output, + Err(err) => vec![format!("{}: {}", nick, err)], + } +} + +pub fn column_renderer_single( + width: usize, + header: &str, + content: Vec>, +) -> Vec { + if content.len() == 0 { + return vec![]; + } + + // first, get the max row + + let total_columns = content[0].len(); + let column_width = ((width - 4) as f32 / total_columns as f32).floor() as usize; + + let mut lines = vec![]; + + lines.push(format!( + "╭ {} {}╮", + header, + "─".repeat(width - header.irc_safe_len() - 4) + )); + + for row in content { + let mut column = String::new(); + + for cell in row { + let value = truncate_string(&cell, column_width); + let spaces = " ".repeat(column_width - value.irc_safe_len()); + column += &format!("{}{}", value, spaces); + } + + column += &" ".repeat(width - column.irc_safe_len() - 4); + lines.push(format!("│ {} │", column)); + } + + lines.push(format!("╰{}╯", "─".repeat(width - 2))); + + lines +} + +pub trait IrcSafeLen { + fn irc_safe_len(&self) -> usize; +} +impl IrcSafeLen for String { + fn irc_safe_len(&self) -> usize { + self.replace("\x0300", "") + .replace("\x0301", "") + .replace("\x0302", "") + .replace("\x0303", "") + .replace("\x0304", "") + .replace("\x0305", "") + .replace("\x0306", "") + .replace("\x0307", "") + .replace("\x0308", "") + .replace("\x0309", "") + .replace("\x0310", "") + .replace("\x0311", "") + .replace("\x0312", "") + .replace("\x0313", "") + .replace("\x0314", "") + .replace("\x0315", "") + .chars() + .filter(|c| !['\x02', '\x1d', '\x1f', '\x1e', '\x12', '\x0f'].contains(c)) + .count() + } +} + +impl IrcSafeLen for &str { + fn irc_safe_len(&self) -> usize { + self.replace("\x0300", "") + .replace("\x0301", "") + .replace("\x0302", "") + .replace("\x0303", "") + .replace("\x0304", "") + .replace("\x0305", "") + .replace("\x0306", "") + .replace("\x0307", "") + .replace("\x0308", "") + .replace("\x0309", "") + .replace("\x0310", "") + .replace("\x0311", "") + .replace("\x0312", "") + .replace("\x0313", "") + .replace("\x0314", "") + .replace("\x0315", "") + .chars() + .filter(|c| !['\x02', '\x1d', '\x1f', '\x1e', '\x12', '\x0f'].contains(c)) + .count() + } +} + +pub fn calc_damage(rng: &mut dyn RngCore, weapon: &Weapon, armor: &Option<(String, Armor)>) -> f32 { + let mut damage = rng.gen_range((weapon.damage / 3.)..=weapon.damage); + + if armor.is_some() { + damage -= + rng.gen_range((armor.as_ref().unwrap().1.block / 2.)..=armor.as_ref().unwrap().1.block); + } + + damage.max(0.) +} diff --git a/drugwars_config.yaml b/drugwars_config.yaml new file mode 100644 index 0000000..616e157 --- /dev/null +++ b/drugwars_config.yaml @@ -0,0 +1,200 @@ +settings: + day_duration: 300 # default is 5 mins (300) + start_day: 1993-04-20 + save_path: save.yaml + width: 120 +locations: + - name: Beijing, China + position: + lat: 39.9042 + long: 116.4074 + - name: Boston, USA + position: + lat: 42.3601 + long: -71.0589 + - name: Detroit, USA + position: + lat: 42.3314 + long: -83.0458 + - name: London, England + position: + lat: 51.5072 + long: -0.1276 + - name: Los Angeles, USA + position: + lat: 34.0522 + long: -118.2437 + - name: Miami, USA + position: + lat: 25.7617 + long: -80.1918 + - name: Mowcow, Russia + position: + lat: 55.7558 + long: 37.6173 + - name: New York, USA + position: + lat: 40.7128 + long: -74.0060 + - name: Paris, France + position: + lat: 48.8566 + long: 2.3522 + - name: San Francisco, USA + position: + lat: 37.7749 + long: -122.4194 + - name: St Petersburg, Russia + position: + lat: 59.9343 + long: -30.3351 + - name: Sydney, Australia + position: + lat: -33.8688 + long: 151.2093 + - name: Toronto, Canada + position: + lat: 43.6532 + long: -79.3832 + - name: Vancouver, Canada + position: + lat: 49.2827 + long: -123.1207 + - name: Bogota, Colombia + position: + lat: 4.7110 + long: -74.0721 + - name: Johannesburg, South Africa + position: + lat: -26.2041 + long: 28.0473 + +drugs: + - name: Cocaine + price: 6500 + - name: Crack + price: 8000 + - name: Ecstasy + price: 3500 + - name: Estradiol + price: 1000 + - name: Fentanyl + price: 1300 + - name: Hashish + price: 600 + - name: Heroin + price: 4000 + - name: Ice + price: 850 + - name: Kat + price: 650 + - name: Krokodil + price: 12 + - name: LSD + price: 2200 + - name: MDA + price: 3300 + - name: Morphine + price: 6200 + - name: Mushrooms + price: 550 + - name: Opium + price: 400 + - name: PCP + price: 1200 + - name: Peyote + price: 800 + - name: Loud + price: 420 + - name: Special K + price: 2700 + - name: Speed + price: 3900 + +items: + + weapons: + - name: Knife + price: 100 + damage: 10 + - name: Pistol + price: 500 + damage: 15 + ammo: Pistol round + - name: Shotgun + price: 2500 + damage: 20 + ammo: Shotgun shell + - name: Machine gun + price: 4000 + damage: 25 + ammo: Machine gun round + - name: Flamethrower + price: 7500000 + damage: 30 + ammo: Gas canister + - name: Rocket launcher + price: 1000000 + damage: 35 + ammo: Rocket + - name: Area disrupter + price: 5000000 + damage: 50 + ammo: Energy globe + + ammos: + - name: Pistol round + price: 5 + - name: Shotgun shell + price: 80 + - name: Machine gun round + price: 1000 + - name: Gas canister + price: 20000 + - name: Rocket + price: 500000 + - name: Energy globe + price: 2500000 + + armors: + - name: Leather coat + price: 10000 + block: 15 + - name: Bulletproof vest + price: 250000 + block: 35 + +messages: + price_up: + - acidvegas failed to synthetize some %DRUG. + - sht got grumpy and destroyed a pile of %DRUG. + - alghazi snorted all the %DRUG and stepped on his rug! + - blowfish made a %DRUG soup but then dropped it on the floor. + - The smuggler of some %DRUG was killed by a rival dealer. + - The pilot of a %DRUG shipment fell asleep and crashed. + - The HIE monster came and ate all the %DRUG. + - Gang warfare is keeping %DRUG off the streets. + - Racoons broke into a crate of %DRUG, eating some and dying on the rest. + - Cops burst into a %DRUG warehouse, seizing everything. + - A big scary monster came and bit %DRUG in the leg. + price_up_end: + - Prices are astronomical! + - Prices are insanely high! + - Prices are outrageous! + - Prices go through the roof! + - Prices are higher than labatu! + - Prices are higher than the people who use your goods! + price_down: + - vap0r came back with a big load of %DRUG. + - A boatload of %DRUG arrives. + - A police warehouse is broken into and %DRUG is stolen. + - A new source of %DRUG is found. + - Crates of %DRUG were discovered floating in the ocean. + - Someone found %DRUG in blowfish bowl. + price_down_end: + - Prices are lower than the Marianas Trench! + - Prices nose dive! + - Prices plummet! + - Prices drop like lead balloons! + - Prices are rock bottom! + diff --git a/irc/Cargo.toml b/irc/Cargo.toml new file mode 100644 index 0000000..2c5817b --- /dev/null +++ b/irc/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "irc" +version = "0.1.0" +authors = ["wrk"] +edition = "2021" + +[dependencies] +serde = { version = "1.0.163", features = ["derive"] } +serde_yaml = "0.9.21" +typemap_rev = "0.3.0" +native-tls = "0.2.11" diff --git a/irc/src/builder.rs b/irc/src/builder.rs new file mode 100644 index 0000000..ed0c5cc --- /dev/null +++ b/irc/src/builder.rs @@ -0,0 +1,193 @@ +use std::{ + collections::{HashMap, VecDeque}, + time::SystemTime, +}; + +use typemap_rev::{TypeMap, TypeMapKey}; + +use crate::{config::IrcConfig, Channel, FloodControl, IdentifyKind, Irc, System}; + +#[derive(Default)] +pub struct IrcBuilder { + host: Option, + port: Option, + ssl: Option, + + channels: Vec, + + nick: Option, + user: Option, + real: Option, + + nickserv_pass: Option, + nickserv_email: Option, + + cmdkey: Option, + + flood_interval: Option, + + data: TypeMap, + + default_system: Option, + systems: HashMap, + admin_systems: HashMap, + + owner: Option, + admins: Vec, +} + +impl From for IrcBuilder { + fn from(config: IrcConfig) -> Self { + Self { + host: Some(config.host), + port: Some(config.port), + ssl: Some(config.ssl), + + channels: config.channels.into_iter().map(Channel::from).collect(), + + nick: Some(config.nick), + user: Some(config.user), + real: Some(config.real), + + nickserv_pass: config.nickserv_pass, + nickserv_email: config.nickserv_email, + + cmdkey: Some(config.cmdkey), + + flood_interval: Some(config.flood_interval), + + data: TypeMap::default(), + + default_system: None, + systems: HashMap::default(), + admin_systems: HashMap::default(), + + owner: Some(config.owner), + admins: config.admins, + } + } +} + +impl IrcBuilder { + pub fn new() -> Self { + Self::default() + } + + pub fn host(&mut self, host: &str) -> &mut Self { + self.host = Some(host.to_owned()); + self + } + + pub fn port(&mut self, port: u16) -> &mut Self { + self.port = Some(port); + self + } + + pub fn ssl(&mut self, ssl: bool) -> &mut Self { + self.ssl = Some(ssl); + self + } + + pub fn channel(&mut self, channel: &str, key: Option<&str>) -> &mut Self { + self.channels.push(Channel { + name: channel.to_owned(), + key: if key.is_some() { + Some(key.unwrap().to_owned()) + } else { + None + }, + }); + self + } + + pub fn nick(&mut self, nick: &str) -> &mut Self { + self.nick = Some(nick.to_owned()); + self + } + + pub fn user(&mut self, user: &str) -> &mut Self { + self.user = Some(user.to_owned()); + self + } + + pub fn real(&mut self, real: &str) -> &mut Self { + self.real = Some(real.to_owned()); + self + } + + pub fn cmdkey(&mut self, cmdkey: &str) -> &mut Self { + self.cmdkey = Some(cmdkey.to_owned()); + self + } + + pub fn add_admin(&mut self, admin: &str) -> &mut Self { + self.admins.push(admin.to_owned()); + self + } + + pub fn add_resource>( + &mut self, + resource: V, + ) -> &mut Self { + self.data.insert::(resource); + self + } + + pub fn add_default_system(&mut self, func: System) -> &mut Self { + self.default_system = Some(func); + self + } + + pub fn add_system(&mut self, system_name: &str, func: System) -> &mut Self { + self.systems.insert(system_name.to_owned(), func); + self + } + + pub fn add_admin_system(&mut self, system_name: &str, func: System) -> &mut Self { + self.admin_systems.insert(system_name.to_owned(), func); + self + } + + pub fn build(&mut self) -> Irc { + let mut flood_controls = HashMap::default(); + for chan in &self.channels { + flood_controls.insert(chan.name.clone(), FloodControl::default()); + } + + Irc { + stream: None, + host: self.host.as_ref().unwrap().clone(), + port: self.port.unwrap_or_default(), + ssl: self.ssl.unwrap_or_default(), + + channels: std::mem::take(&mut self.channels), + flood_controls, + + nick: self.nick.as_ref().unwrap().clone(), + user: self.user.as_ref().unwrap().clone(), + real: self.real.as_ref().unwrap().clone(), + + nickserv_pass: self.nickserv_pass.clone(), + nickserv_email: self.nickserv_email.clone(), + + cmdkey: self.cmdkey.as_ref().unwrap().clone(), + + flood_interval: self.flood_interval.unwrap(), + + data: std::mem::take(&mut self.data), + + default_system: self.default_system, + systems: std::mem::take(&mut self.systems), + admin_systems: std::mem::take(&mut self.admin_systems), + + send_queue: VecDeque::new(), + recv_queue: VecDeque::new(), + + owner: self.owner.as_ref().unwrap().clone(), + admins: std::mem::take(&mut self.admins), + + partial_line: String::new(), + identify_kind: IdentifyKind::None(SystemTime::now()), + } + } +} diff --git a/irc/src/config.rs b/irc/src/config.rs new file mode 100644 index 0000000..a95800f --- /dev/null +++ b/irc/src/config.rs @@ -0,0 +1,43 @@ +use std::{fs::File, io::Read}; + +use serde::Deserialize; + +#[derive(Deserialize)] +pub(crate) struct ChannelConfig { + pub(crate) name: String, + pub(crate) key: Option, +} + +#[derive(Deserialize)] +pub(crate) struct IrcConfig { + pub(crate) host: String, + pub(crate) port: u16, + pub(crate) ssl: bool, + + pub(crate) channels: Vec, + + pub(crate) nick: String, + pub(crate) user: String, + pub(crate) real: String, + + pub(crate) nickserv_pass: Option, + pub(crate) nickserv_email: Option, + + pub(crate) cmdkey: String, + + pub(crate) flood_interval: f32, + + pub(crate) owner: String, + pub(crate) admins: Vec, +} + +impl IrcConfig { + pub fn from_file(path: &str) -> std::io::Result { + let mut file = File::open(path)?; + let mut contents = String::new(); + file.read_to_string(&mut contents)?; + + let config: IrcConfig = serde_yaml::from_str(&contents).unwrap(); + Ok(config) + } +} diff --git a/irc/src/format.rs b/irc/src/format.rs new file mode 100644 index 0000000..3c49728 --- /dev/null +++ b/irc/src/format.rs @@ -0,0 +1,67 @@ +#[derive(Clone, Copy)] +pub enum IrcFormat { + Bold, + Italics, + Underline, + Strikethrough, + Reverse, + Color, + Plain, +} + +impl std::fmt::Display for IrcFormat { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + IrcFormat::Bold => write!(f, "\x02"), + IrcFormat::Italics => write!(f, "\x1d"), + IrcFormat::Underline => write!(f, "\x1f"), + IrcFormat::Strikethrough => write!(f, "\x1e"), + IrcFormat::Reverse => write!(f, "\x12"), + IrcFormat::Color => write!(f, "\x03"), + IrcFormat::Plain => write!(f, "\x0f"), + } + } +} + +#[derive(Clone, Copy)] +pub enum IrcColor { + White, + Black, + Blue, + Green, + Red, + Brown, + Purple, + Orange, + Yellow, + LightGreen, + Teal, + Cyan, + LightBlue, + Magenta, + Gray, + LightGray, +} + +impl std::fmt::Display for IrcColor { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + IrcColor::White => write!(f, "00"), + IrcColor::Black => write!(f, "01"), + IrcColor::Blue => write!(f, "02"), + IrcColor::Green => write!(f, "03"), + IrcColor::Red => write!(f, "04"), + IrcColor::Brown => write!(f, "05"), + IrcColor::Purple => write!(f, "06"), + IrcColor::Orange => write!(f, "07"), + IrcColor::Yellow => write!(f, "08"), + IrcColor::LightGreen => write!(f, "09"), + IrcColor::Teal => write!(f, "10"), + IrcColor::Cyan => write!(f, "11"), + IrcColor::LightBlue => write!(f, "12"), + IrcColor::Magenta => write!(f, "13"), + IrcColor::Gray => write!(f, "14"), + IrcColor::LightGray => write!(f, "15"), + } + } +} diff --git a/irc/src/irc_command.rs b/irc/src/irc_command.rs new file mode 100644 index 0000000..ae1497e --- /dev/null +++ b/irc/src/irc_command.rs @@ -0,0 +1,242 @@ +macro_rules! make_irc_command_enum { + + ($variant:ident) => { + $variant + }; + + ($($variant:ident: $value:expr),+) => { + #[allow(non_camel_case_types)] + pub enum IrcCommand { + UNKNOWN, + $($variant),+ + } + + impl From<&str> for IrcCommand { + fn from(command_str: &str) -> Self { + match command_str { + $($value => Self::$variant,)+ + _ => Self::UNKNOWN, + } + } + } + + }; +} + +make_irc_command_enum!( + ADMIN: "ADMIN", + AWAY: "AWAY", + CNOTICE: "CNOTICE", + CPRIVMSG: "CPRIVMSG", + CONNECT: "CONNECT", + DIE: "DIE", + ENCAP: "ENCAP", + ERROR: "ERROR", + HELP: "HELP", + INFO: "INFO", + INVITE: "INVITE", + ISON: "ISON", + JOIN: "JOIN", + KICK: "KICK", + KILL: "KILL", + KNOCK: "KNOCK", + LINKS: "LINKS", + LIST: "LIST", + LUSERS: "LUSERS", + MODE: "MODE", + MOTD: "MOTD", + NAMES: "NAMES", + NICK: "NICK", + NOTICE: "NOTICE", + OPER: "OPER", + PART: "PART", + PASS: "PASS", + PING: "PING", + PONG: "PONG", + PRIVMSG: "PRIVMSG", + QUIT: "QUIT", + REHASH: "REHASH", + RULES: "RULES", + SERVER: "SERVER", + SERVICE: "SERVICE", + SERVLIST: "SERVLIST", + SQUERY: "SQUERY", + SQUIT: "SQUIT", + SETNAME: "SETNAME", + SILENCE: "SILENCE", + STATS: "STATS", + SUMMON: "SUMMON", + TIME: "TIME", + TOPIC: "TOPIC", + TRACE: "TRACE", + USER: "USER", + USERHOST: "USERHOST", + USERIP: "USERIP", + USERS: "USERS", + VERSION: "VERSION", + WALLOPS: "WALLOPS", + WATCH: "WATCH", + WHO: "WHO", + WHOIS: "WHOIS", + WHOWAS: "WHOWAS", + RPL_WELCOME: "001", + RPL_YOURHOST: "002", + RPL_CREATED: "003", + RPL_MYINFO: "004", + RPL_BOUNCE: "005", + RPL_TRACELINK: "200", + RPL_TRACECONNECTING: "201", + RPL_TRACEHANDSHAKE: "202", + RPL_TRACEUNKNOWN: "203", + RPL_TRACEOPERATOR: "204", + RPL_TRACEUSER: "205", + RPL_TRACESERVER: "206", + RPL_TRACESERVICE: "207", + RPL_TRACENEWTYPE: "208", + RPL_TRACECLASS: "209", + RPL_TRACERECONNECT: "210", + RPL_STATSLINKINFO: "211", + RPL_STATSCOMMANDS: "212", + RPL_STATSCLINE: "213", + RPL_STATSNLINE: "214", + RPL_STATSILINE: "215", + RPL_STATSKLINE: "216", + RPL_STATSQLINE: "217", + RPL_STATSYLINE: "218", + RPL_ENDOFSTATS: "219", + RPL_UMODEIS: "221", + RPL_SERVICEINFO: "231", + RPL_ENDOFSERVICES: "232", + RPL_SERVICE: "233", + RPL_SERVLIST: "234", + RPL_SERVLISTEND: "235", + RPL_STATSVLINE: "240", + RPL_STATSLLINE: "241", + RPL_STATSUPTIME: "242", + RPL_STATSOLINE: "243", + RPL_STATSHLINE: "244", + RPL_STATSPING: "246", + RPL_STATSBLINE: "247", + RPL_STATSDLINE: "250", + RPL_LUSERCLIENT: "251", + RPL_LUSEROP: "252", + RPL_LUSERUNKNOWN: "253", + RPL_LUSERCHANNELS: "254", + RPL_LUSERME: "255", + RPL_ADMINME: "256", + RPL_ADMINLOC1: "257", + RPL_ADMINLOC2: "258", + RPL_ADMINEMAIL: "259", + RPL_TRACELOG: "261", + RPL_TRACEEND: "262", + RPL_TRYAGAIN: "263", + RPL_NONE: "300", + RPL_AWAY: "301", + RPL_USERHOST: "302", + RPL_ISON: "303", + RPL_UNAWAY: "305", + RPL_NOWAWAY: "306", + RPL_WHOISUSER: "311", + RPL_WHOISSERVER: "312", + RPL_WHOISOPERATOR: "313", + RPL_WHOWASUSER: "314", + RPL_ENDOFWHO: "315", + RPL_WHOISCHANOP: "316", + RPL_WHOISIDLE: "317", + RPL_ENDOFWHOIS: "318", + RPL_WHOISCHANNELS: "319", + RPL_LISTSTART: "321", + RPL_LIST: "322", + RPL_LISTEND: "323", + RPL_CHANNELMODEIS: "324", + RPL_UNIQOPIS: "325", + RPL_NOTOPIC: "331", + RPL_TOPIC: "332", + RPL_INVITING: "341", + RPL_SUMMONING: "342", + RPL_INVITELIST: "346", + RPL_ENDOFINVITELIST: "347", + RPL_EXCEPTLIST: "348", + RPL_ENDOFEXCEPTLIST: "349", + RPL_VERSION: "351", + RPL_WHOREPLY: "352", + RPL_NAMREPLY: "353", + RPL_KILLDONE: "361", + RPL_CLOSING: "362", + RPL_CLOSEEND: "363", + RPL_LINKS: "364", + RPL_ENDOFLINKS: "365", + RPL_ENDOFNAMES: "366", + RPL_BANLIST: "367", + RPL_ENDOFBANLIST: "368", + RPL_ENDOFWHOWAS: "369", + RPL_INFO: "371", + RPL_MOTD: "372", + RPL_INFOSTART: "373", + RPL_ENDOFINFO: "374", + RPL_MOTDSTART: "375", + RPL_ENDOFMOTD: "376", + RPL_YOUREOPER: "381", + RPL_REHASHING: "382", + RPL_YOURESERVICE: "383", + RPL_MYPORTIS: "384", + RPL_TIME: "391", + RPL_USERSSTART: "392", + RPL_USERS: "393", + RPL_ENDOFUSERS: "394", + RPL_NOUSERS: "395", + ERR_NOSUCHNICK: "401", + ERR_NOSUCHSERVER: "402", + ERR_NOSUCHCHANNEL: "403", + ERR_CANNOTSENDTOCHAN: "404", + ERR_TOOMANYCHANNELS: "405", + ERR_WASNOSUCHNICK: "406", + ERR_TOOMANYTARGETS: "407", + ERR_NOSUCHSERVICE: "408", + ERR_NOORIGIN: "409", + ERR_NORECIPIENT: "411", + ERR_NOTEXTTOSEND: "412", + ERR_NOTOPLEVEL: "413", + ERR_WILDTOPLEVEL: "414", + ERR_BADMASK: "415", + ERR_UNKNOWNCOMMAND: "421", + ERR_NOMOTD: "422", + ERR_NOADMININFO: "423", + ERR_FILEERROR: "424", + ERR_NONICKNAMEGIVEN: "431", + ERR_ERRONEUSNICKNAME: "432", + ERR_NICKNAMEINUSE: "433", + ERR_NICKCOLLISION: "436", + ERR_UNAVAILRESOURCE: "437", + ERR_USERNOTINCHANNEL: "441", + ERR_NOTONCHANNEL: "442", + ERR_USERONCHANNEL: "443", + ERR_NOLOGIN: "444", + ERR_SUMMONDISABLED: "445", + ERR_USERSDISABLED: "446", + ERR_NOTREGISTERED: "451", + ERR_NEEDMOREPARAMS: "461", + ERR_ALREADYREGISTERED: "462", + ERR_NOPERMFORHOST: "463", + ERR_PASSWDMISMATCH: "464", + ERR_YOUREBANNEDCREEP: "465", + ERR_YOUWILLBEBANNED: "466", + ERR_KEYSET: "467", + ERR_CHANNELISFULL: "471", + ERR_UNKNOWNMODE: "472", + ERR_INVITEONLYCHAN: "473", + ERR_BANNEDFROMCHAN: "474", + ERR_BADCHANNELKEY: "475", + ERR_BADCHANMASK: "476", + ERR_NOCHANMODES: "477", + ERR_BANLISTFULL: "478", + ERR_NOPRIVILEGES: "481", + ERR_CHANOPRIVSNEEDED: "482", + ERR_CANTKILLSERVER: "483", + ERR_RESTRICTED: "484", + ERR_UNIQOPRIVSNEEDED: "485", + ERR_NOOPERHOST: "491", + ERR_NOSERVICEHOST: "492", + ERR_UMODEUNKNOWNFLAG: "501", + ERR_USERSDONTMATCH: "502" +); diff --git a/irc/src/lib.rs b/irc/src/lib.rs new file mode 100644 index 0000000..ddaa1fb --- /dev/null +++ b/irc/src/lib.rs @@ -0,0 +1,679 @@ +/// +/// TODO: impl colors ^-^ +/// +extern crate typemap_rev; + +use std::{ + collections::{HashMap, VecDeque}, + io::{ErrorKind, Read, Write}, + net::{TcpStream, ToSocketAddrs}, + time::{Duration, SystemTime}, +}; + +use builder::IrcBuilder; +use config::{ChannelConfig, IrcConfig}; +use irc_command::IrcCommand; +use native_tls::{TlsConnector, TlsStream}; +use typemap_rev::TypeMap; + +pub mod builder; +pub mod config; +pub mod format; +pub mod irc_command; +pub mod privmsg; + +pub mod typemap { + pub use typemap_rev::*; +} + +pub(crate) const MAX_MSG_LEN: usize = 512; +pub(crate) type System = fn(&mut Irc, &IrcPrefix, Vec<&str>) -> Vec; + +pub enum Stream { + Plain(TcpStream), + Tls(TlsStream), +} + +impl Stream { + pub fn read(&mut self, buf: &mut [u8]) -> std::result::Result { + match self { + Stream::Plain(stream) => stream.read(buf), + Stream::Tls(stream) => stream.read(buf), + } + } + + pub fn write(&mut self, buf: &[u8]) -> std::result::Result { + match self { + Stream::Plain(stream) => stream.write(buf), + Stream::Tls(stream) => stream.write(buf), + } + } +} + +#[derive(Debug, Default)] +pub struct IrcPrefix<'a> { + pub admin: bool, + pub nick: &'a str, + pub user: Option<&'a str>, + pub host: Option<&'a str>, +} + +impl<'a> From<&'a str> for IrcPrefix<'a> { + fn from(prefix_str: &'a str) -> Self { + let prefix_str = &prefix_str[1..]; + + let nick_split: Vec<&str> = prefix_str.split('!').collect(); + let nick = nick_split[0]; + + // we only have a nick + if nick_split.len() == 1 { + return Self { + nick, + ..Default::default() + }; + } + + let user_split: Vec<&str> = nick_split[1].split('@').collect(); + let user = user_split[0]; + + // we don't have an host + if user_split.len() == 1 { + return Self { + nick: nick, + user: Some(user), + ..Default::default() + }; + } + + Self { + admin: false, + nick: nick, + user: Some(user), + host: Some(user_split[1]), + } + } +} + +pub struct IrcMessage<'a> { + prefix: Option>, + command: IrcCommand, + parameters: Vec<&'a str>, +} + +impl<'a> From<&'a str> for IrcMessage<'a> { + fn from(line: &'a str) -> Self { + let mut elements = line.split_whitespace(); + + let tmp = elements.next().unwrap(); + + if tmp.chars().next().unwrap() == ':' { + return Self { + prefix: Some(tmp.into()), + command: elements.next().unwrap().into(), + parameters: elements.collect(), + }; + } + + Self { + prefix: None, + command: tmp.into(), + parameters: elements.collect(), + } + } +} + +#[derive(Clone)] +pub struct Channel { + name: String, + key: Option, +} + +impl From for Channel { + fn from(channel_config: ChannelConfig) -> Self { + Self { + name: channel_config.name, + key: channel_config.key, + } + } +} + +pub struct FloodControl { + last_cmd: SystemTime, +} + +impl Default for FloodControl { + fn default() -> Self { + Self { + last_cmd: SystemTime::now(), + } + } +} + +#[derive(PartialEq, Eq)] +pub enum IdentifyKind { + Identified, + Registered, + None(SystemTime), +} + +pub struct Irc { + stream: Option, + host: String, + port: u16, + ssl: bool, + + channels: Vec, + flood_controls: HashMap, + + nick: String, + user: String, + real: String, + + nickserv_pass: Option, + nickserv_email: Option, + + cmdkey: String, + + flood_interval: f32, + + data: TypeMap, + + default_system: Option, + systems: HashMap, + admin_systems: HashMap, + + send_queue: VecDeque, + recv_queue: VecDeque, + + owner: String, + admins: Vec, + + partial_line: String, + + identify_kind: IdentifyKind, +} + +impl Irc { + pub fn from_config(config_path: &str) -> IrcBuilder { + let config = IrcConfig::from_file(config_path).unwrap(); + config.into() + } + + pub fn new() -> IrcBuilder { + IrcBuilder::new() + } + + pub fn data(&self) -> &TypeMap { + &self.data + } + + pub fn data_mut(&mut self) -> &mut TypeMap { + &mut self.data + } + + pub fn connect(&mut self) -> std::result::Result<(), std::io::Error> { + let domain = format!("{}:{}", self.host, self.port); + + let mut addrs = domain + .to_socket_addrs() + .expect("Unable to get addrs from domain {domain}"); + + let sock = addrs + .next() + .expect("Unable to get ip from addrs: {addrs:?}"); + + let stream = TcpStream::connect(sock)?; + stream.set_nonblocking(true)?; + + if self.ssl { + let connector = TlsConnector::builder() + .danger_accept_invalid_certs(true) + .danger_accept_invalid_hostnames(true) + .build() + .unwrap(); + + let mut tls_stream = connector.connect(&self.host, stream); + + while tls_stream.is_err() { + tls_stream = match tls_stream.err().unwrap() { + native_tls::HandshakeError::Failure(f) => panic!("{f}"), + native_tls::HandshakeError::WouldBlock(mid_handshake) => { + mid_handshake.handshake() + } + } + } + + self.stream = Some(Stream::Tls(tls_stream.unwrap())); + return Ok(()); + } + + self.stream = Some(Stream::Plain(stream)); + Ok(()) + } + + fn join(&mut self) { + for i in 0..self.channels.len() { + let channel = &self.channels[i]; + if channel.key.is_some() { + self.queue(&format!( + "JOIN {} {}", + channel.name, + channel.key.as_ref().unwrap() + )) + } else { + self.queue(&format!("JOIN {}", channel.name)) + } + } + } + + fn join_manual(&mut self, channel: &str, key: Option<&str>) { + if key.is_some() { + self.queue(&format!("JOIN {} {}", channel, key.unwrap())); + } else { + self.queue(&format!("JOIN {}", channel)); + } + } + + pub fn register(&mut self) { + self.queue(&format!("USER {} 0 * {}", self.user, self.real)); + self.queue(&format!("NICK {}", self.nick)); + } + + pub fn run(&mut self) { + // main loop! + loop { + self.recv().unwrap(); + self.send().unwrap(); + self.handle_commands(); + + std::thread::sleep(Duration::from_millis(50)); + } + } + + pub fn update(&mut self) { + self.nickserv(); + self.recv().unwrap(); + self.send().unwrap(); + self.handle_commands(); + } + + fn nickserv(&mut self) { + if self.identify_kind == IdentifyKind::Identified { + return; + } + + if self.identify_kind == IdentifyKind::Registered { + return self.identify(); + } + + let IdentifyKind::None(since) = self.identify_kind else { return; }; + + if self.nickserv_pass.is_none() { + return; + } + + let Ok(elapsed) = since.elapsed() else { return; }; + + if elapsed.as_secs() > 15 { + match &self.nickserv_email { + Some(email) => self.privmsg( + "NickServ", + &format!("REGISTER {} {}", self.nickserv_pass.clone().unwrap(), email), + ), + None => self.privmsg( + "NickServ", + &format!("REGISTER {}", self.nickserv_pass.clone().unwrap()), + ), + }; + self.identify_kind = IdentifyKind::None(SystemTime::now()); + } + } + + fn recv(&mut self) -> Result<(), std::io::Error> { + let Some(stream) = &mut self.stream else { panic!("stream gwan boom."); }; + + let mut lines = VecDeque::new(); + + loop { + let mut buf = [0; MAX_MSG_LEN]; + + let bytes_read = match stream.read(&mut buf) { + Ok(bytes_read) => bytes_read, + Err(err) => match err.kind() { + ErrorKind::WouldBlock => { + self.recv_queue.append(&mut lines); + return Ok(()); + } + _ => panic!("{err}"), + }, + }; + + if bytes_read == 0 { + break; + } + + let buf = &buf[..bytes_read]; + + let mut str_buf = self.partial_line.clone(); + str_buf += String::from_utf8_lossy(buf).into_owned().as_str(); + let new_lines: Vec<&str> = str_buf.split("\r\n").collect(); + let len = new_lines.len(); + + for (index, line) in new_lines.into_iter().enumerate() { + if index == len - 1 { + self.partial_line = line.to_owned(); + break; + } + lines.push_back(line.to_owned()); + } + } + Ok(()) + } + + fn send(&mut self) -> Result<(), std::io::Error> { + let Some(stream) = &mut self.stream else { panic!("stream gwan boom."); }; + + while self.send_queue.len() > 0 { + let msg = self.send_queue.pop_front().unwrap(); + + let bytes_written = match stream.write(msg.as_bytes()) { + Ok(bytes_written) => bytes_written, + Err(err) => match err.kind() { + ErrorKind::WouldBlock => { + println!("would block send."); + return Ok(()); + } + _ => panic!("{err}"), + }, + }; + + if bytes_written < msg.len() { + self.send_queue.push_front(msg[bytes_written..].to_owned()); + } + } + + Ok(()) + } + + fn handle_commands(&mut self) { + while self.recv_queue.len() != 0 { + let owned_line = self.recv_queue.pop_front().unwrap(); + let line = owned_line.as_str(); + + println!("<< {:?}", line); + + let mut message: IrcMessage = line.into(); + + let Some(prefix) = &mut message.prefix else { + return self.handle_message(&message); + }; + + if self.is_owner(prefix) { + prefix.admin = true; + } else { + for admin in &self.admins { + if self.is_admin(prefix, admin) { + prefix.admin = true; + break; + } + } + } + + self.handle_message(&message); + } + } + + fn handle_message(&mut self, message: &IrcMessage) { + match message.command { + IrcCommand::PING => self.event_ping(&message.parameters[0]), + IrcCommand::RPL_WELCOME => self.event_welcome(), + IrcCommand::ERR_NICKNAMEINUSE => self.update_nick(&format!("{}_", &self.nick)), + IrcCommand::KICK => self.event_kick( + message.parameters[0], + message.parameters[1], + &message.parameters[3..].join(" "), + ), + IrcCommand::QUIT => self.event_quit(message.prefix.as_ref().unwrap()), + IrcCommand::INVITE => self.event_invite( + message.prefix.as_ref().unwrap(), + &message.parameters[0][1..], + ), + IrcCommand::PRIVMSG => self.event_privmsg( + message.prefix.as_ref().unwrap(), + &message.parameters[0], + &message.parameters[1..].join(" ")[1..], + ), + IrcCommand::JOIN => self.event_join( + message.prefix.as_ref().unwrap(), + &message.parameters[0][1..], + ), + IrcCommand::NOTICE => self.event_notice( + message.prefix.as_ref(), + &message.parameters[0], + &message.parameters[1..].join(" ")[1..], + ), + _ => {} + } + } + + fn queue(&mut self, msg: &str) { + let mut msg = msg.replace("\r", "").replace("\n", ""); + + if msg.len() > MAX_MSG_LEN - "\r\n".len() { + let mut i = 0; + + while i < msg.len() { + let max = (MAX_MSG_LEN - "\r\n".len()).min(msg[i..].len()); + + let mut m = msg[i..(i + max)].to_owned(); + println!(">> {:?}", m); + m = m + "\r\n"; + self.send_queue.push_back(m); + i += MAX_MSG_LEN - "\r\n".len() + } + } else { + println!(">> {:?}", msg); + msg = msg + "\r\n"; + self.send_queue.push_back(msg); + } + } + + fn event_ping(&mut self, ping_token: &str) { + self.queue(&format!("PONG {}", ping_token)); + } + + fn event_welcome(&mut self) { + self.identify(); + self.join(); + } + + fn identify(&mut self) { + let Some(nickserv_pass) = self.nickserv_pass.clone() else { return; }; + self.privmsg("NickServ", &format!("IDENTIFY {}", nickserv_pass)); + } + + fn update_nick(&mut self, new_nick: &str) { + self.nick = new_nick.to_owned(); + self.queue(&format!("NICK {}", self.nick)); + } + + fn event_kick(&mut self, channel: &str, nick: &str, message: &str) { + if nick != &self.nick { + return; + } + + println!("we got kicked!"); + println!("{message}"); + + //TODO: fix this in case a key is needed. + self.join_manual(channel, None); + } + + fn event_quit(&mut self, prefix: &IrcPrefix) { + if prefix.nick != self.nick { + return; + } + + println!("need to reconnect."); + std::thread::sleep(Duration::from_secs(15)); + self.connect().unwrap(); + self.register(); + } + + fn event_invite(&mut self, prefix: &IrcPrefix, channel: &str) { + println!("{} invited us to {}", prefix.nick, channel); + } + + fn execute_default(&mut self, prefix: &IrcPrefix, channel: &str, message: &str) { + let Some(default_func) = self.default_system else { return; }; + + let mut elements = message.split_whitespace(); + elements.next(); + + let output = default_func(self, prefix, elements.collect()); + + if output.len() == 0 { + return; + } + for line in output { + self.privmsg(channel, &line); + } + } + + fn is_flood(&mut self, channel: &str) -> bool { + let mut flood_control = match self.flood_controls.entry(channel.to_owned()) { + std::collections::hash_map::Entry::Occupied(o) => o.into_mut(), + std::collections::hash_map::Entry::Vacant(v) => v.insert(FloodControl { + last_cmd: SystemTime::now(), + }), + }; + + let elapsed = flood_control.last_cmd.elapsed().unwrap(); + + if elapsed.as_secs_f32() < self.flood_interval { + return true; + } + + flood_control.last_cmd = SystemTime::now(); + false + } + + fn event_privmsg(&mut self, prefix: &IrcPrefix, channel: &str, message: &str) { + if message.starts_with(&self.cmdkey) { + let mut elements = message.split_whitespace(); + let sys_name = &elements.next().unwrap()[1..]; + + if self.is_owner(prefix) && sys_name == "raw" { + self.queue(&elements.collect::>().join(" ")); + return; + } + + if self.is_flood(channel) { + return; + } + + if prefix.admin { + if let Some(admin_func) = self.admin_systems.get(sys_name) { + let output = admin_func(self, prefix, elements.collect()); + + if output.len() == 0 { + return; + } + + for line in output { + self.privmsg(channel, &line); + } + + return; + }; + } + + let Some(func) = self.systems.get(sys_name) else { + self.execute_default(prefix, channel, message); + return; + }; + + let output = func(self, prefix, elements.collect()); + + if output.len() == 0 { + return; + } + + for line in output { + self.privmsg(channel, &line); + } + } + } + + fn event_notice(&mut self, prefix: Option<&IrcPrefix>, channel: &str, message: &str) { + if prefix.is_none() { + return; + } + let prefix = prefix.unwrap(); + + if prefix.nick != "NickServ" || channel != &self.nick { + return; + } + + if message == format!("Nick \x02{}\x02 isn't registered.", self.nick) + && self.nickserv_pass.is_some() + { + match &self.nickserv_email { + Some(email) => self.privmsg( + "NickServ", + &format!("REGISTER {} {}", self.nickserv_pass.clone().unwrap(), email), + ), + None => self.privmsg( + "NickServ", + &format!("REGISTER {}", self.nickserv_pass.clone().unwrap()), + ), + } + } + + if message == format!("Nick \x02{}\x02 registered.", self.nick) + || message == format!("Nickname \x02{}\x02 registered.", self.nick) + && self.nickserv_pass.is_some() + { + self.identify_kind = IdentifyKind::Identified; + } + + if message == "Password accepted - you are now recognized." { + self.identify_kind = IdentifyKind::Identified; + } + } + + fn event_join(&mut self, prefix: &IrcPrefix, _channel: &str) { + if prefix.nick != self.nick { + return; + } + } + + pub fn privmsg(&mut self, channel: &str, message: &str) { + self.queue(&format!("PRIVMSG {} :{}", channel, message)); + } + + pub fn privmsg_all(&mut self, message: &str) { + for i in 0..self.channels.len() { + let channel = &self.channels[i]; + self.queue(&format!("PRIVMSG {} :{}", channel.name, message)); + } + } + + fn is_owner(&self, prefix: &IrcPrefix) -> bool { + self.is_admin(prefix, &self.owner) + } + + fn is_admin(&self, prefix: &IrcPrefix, admin: &str) -> bool { + let admin = ":".to_owned() + &admin; + let admin_prefix: IrcPrefix = admin.as_str().into(); + + if (admin_prefix.nick == prefix.nick || admin_prefix.nick == "*") + && (admin_prefix.user == prefix.user || admin_prefix.user == Some("*")) + && (admin_prefix.host == prefix.host || admin_prefix.host == Some("*")) + { + return true; + } + + false + } +} diff --git a/irc/src/privmsg.rs b/irc/src/privmsg.rs new file mode 100644 index 0000000..1ece1f1 --- /dev/null +++ b/irc/src/privmsg.rs @@ -0,0 +1,33 @@ +use crate::format::{IrcColor, IrcFormat}; +pub struct PrivMsg(String); + +impl PrivMsg { + pub fn new() -> Self { + Self(String::new()) + } + + pub fn color(&mut self, color: IrcColor) -> &mut Self { + self.0 += &IrcFormat::Color.to_string(); + self.0 += &color.to_string(); + self + } + + pub fn format(&mut self, format: IrcFormat) -> &mut Self { + self.0 += &format.to_string(); + self + } + + pub fn reset(&mut self) -> &mut Self { + self.0 += &IrcFormat::Plain.to_string(); + self + } + + pub fn text(&mut self, text: &str) -> &mut Self { + self.0 += text; + self + } + + pub fn get(&self) -> &str { + &self.0 + } +} diff --git a/irc_config.example.yaml b/irc_config.example.yaml new file mode 100644 index 0000000..389d050 --- /dev/null +++ b/irc_config.example.yaml @@ -0,0 +1,21 @@ +host: irc.supernets.org +port: 6697 +ssl: true + +channels: + - name: "#drugwars" + +nick: KINGPIN +user: drugwars +real: drugwars + +nickserv_pass: REDACTED +nickserv_email: REDACTED + +cmdkey: . + +flood_interval: 1 + +owner: "*!*@we.gettin.doped.sh" + +admins: []