initial commit

This commit is contained in:
wrk 2023-05-27 09:37:54 +00:00
commit 6436bd9f2c
25 changed files with 5720 additions and 0 deletions

4
.gitignore vendored Normal file
View File

@ -0,0 +1,4 @@
/target
/Cargo.lock
save.yaml
irc_config.yaml

6
Cargo.toml Normal file
View File

@ -0,0 +1,6 @@
[workspace]
members = [
"irc",
"drugwars"
]

3
README.md Normal file
View File

@ -0,0 +1,3 @@
# Drugwars
copy `irc_config.example.yaml` to `irc_config.yaml` and edit it before starting the bot.

13
drugwars/Cargo.toml Normal file
View File

@ -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"] }

126
drugwars/src/admin.rs Normal file
View File

@ -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 <nick>
*/
impl DrugWars {
pub fn admin_dealer(&mut self, nick: &str, arguments: &[&str]) -> Result<Vec<String>> {
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<Vec<String>> {
if args.len() < 1 {
return Ok(self.render_command_list());
}
match cmd {
"money" => {
let amount = (self.get_amount_from_str::<f64>(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::<f64>(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::<f32>(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::<usize>(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::<Location>(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![]),
}
}
}

463
drugwars/src/api.rs Normal file
View File

@ -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<Vec<String>> {
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<Vec<String>> {
let dealer = self.get_dealer(nick)?;
Ok(self.render_market(nick, dealer))
}
pub fn show_info(&self, nick: &str) -> Result<Vec<String>> {
let dealer = self.get_dealer(nick)?;
Ok(self.render_info(nick, dealer))
}
pub fn show_people(&self, nick: &str) -> Result<Vec<String>> {
let dealer = self.get_dealer(nick)?;
Ok(self.render_people(dealer))
}
pub fn show_date_time(&self) -> Result<Vec<String>> {
Ok(self.render_time())
}
pub fn show_all_commands(&self) -> Result<Vec<String>> {
Ok(self.render_command_list())
}
pub fn leaderboard(&self) -> Result<Vec<String>> {
Ok(self.render_leaderboard())
}
pub fn show_admin_commands(&self) -> Result<Vec<String>> {
Ok(self.render_admin_command_list())
}
pub fn buy<T: DealerComponent + Matchable>(
&mut self,
nick: &str,
name_str: &str,
amount_str: &str,
) -> Result<Vec<String>> {
let dealer = self.get_dealer_available(nick)?;
let (name, _) = self.get_matching::<T>(&name_str)?;
let amount = self.get_buy_amount_of::<T>(dealer, name, amount_str)?;
let location = self.locations.get(&dealer.location).unwrap();
let market = location.get_market::<T>();
if !market.contains_key(name) {
return Err(Error::NoElementAtMarket(name.to_owned()));
}
if dealer.get_total_owned_local::<T>() + 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::<T>(nick, &name.clone(), amount, elem_at_market.price)
}
pub fn sell<T: DealerComponent + Matchable>(
&mut self,
nick: &str,
name_str: &str,
amount_str: &str,
) -> Result<Vec<String>> {
let dealer = self.get_dealer_available(nick)?;
let (name, _) = self.get_matching::<T>(&name_str)?;
let amount = self.get_sell_amount_of::<T>(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::<T>();
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::<T>(nick, &name.clone(), amount)
}
pub fn give_money(
&mut self,
nick: &str,
amount_str: &str,
bloke_nick: &str,
) -> Result<Vec<String>> {
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<T: DealerComponent + Matchable>(
&mut self,
nick: &str,
name_str: &str,
amount_str: &str,
bloke_nick: &str,
) -> Result<Vec<String>> {
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::<T>(&name_str)?;
let owned_element_local = dealer.get_owned_local::<T>();
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::<T>(dealer, bloke, name, amount_str)?;
if owned_element.amount < amount {
return Err(Error::NotEnoughElementOwned(name.to_owned()));
}
if bloke.get_total_owned_local::<T>() + amount > bloke.capacity {
return Err(Error::BlokeNotEnoughCapacity(bloke_nick.to_owned()));
}
self._give::<T>(nick, bloke_nick, &name.clone(), amount)
}
pub fn check_flight_prices(&self, nick: &str) -> Result<Vec<String>> {
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<Vec<String>> {
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<Vec<String>> {
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<T: DealerComponent + Matchable>(
&mut self,
nick: &str,
name_str: &str,
amount_str: &str,
destination_str: &str,
) -> Result<Vec<String>> {
let dealer = self.get_dealer_available(nick)?;
let (name, _) = self.get_matching::<T>(&name_str)?;
if !dealer.get_owned_local::<T>().contains_key(name) {
return Err(Error::NoElementOwned(name.to_owned()));
}
let (destination_name, destination) = self.get_matching::<Location>(&destination_str)?;
if dealer.location == *destination_name {
return Err(Error::ShipCurrentLocation);
}
let amount = self.get_ship_amount_of::<T>(dealer, destination_name, name, amount_str)?;
let target_remaining_capacity = dealer.get_remaining_capacity_at::<T>(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<T: DealerComponent + Matchable>(
&mut self,
nick: &str,
name_str: &str,
amount_str: &str,
destination_str: &str,
) -> Result<Vec<String>> {
let dealer = self.get_dealer_available(nick)?;
let (name, _) = self.get_matching::<T>(&name_str)?;
if !dealer.get_owned_local::<T>().contains_key(name) {
return Err(Error::NoElementOwned(name.to_owned()));
}
let (destination_name, destination) = self.get_matching::<Location>(&destination_str)?;
if dealer.location == *destination_name {
return Err(Error::ShipCurrentLocation);
}
let amount = self.get_ship_amount_of::<T>(dealer, &destination_name, name, amount_str)?;
let owned_local = dealer.get_owned_local::<T>();
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::<T>(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::<T>(nick, &name.clone(), amount, &destination_name.clone())
}
pub fn buy_capacity(&mut self, nick: &str, amount_str: &str) -> Result<Vec<String>> {
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<Vec<String>> {
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::<Item>(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<Vec<String>> {
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<Vec<String>> {
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<Vec<String>> {
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))
}
}

27
drugwars/src/config.rs Normal file
View File

@ -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<String>,
}
impl DrugWarsConfig {
pub fn from_file(path: &str) -> std::io::Result<Self> {
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)
}
}

212
drugwars/src/dealer.rs Normal file
View File

@ -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<String, HashMap<String, OwnedElement>>,
pub owned_items: HashMap<String, HashMap<String, OwnedElement>>,
pub cartel_payroll: u128,
pub cartel_health: f32,
pub status: DealerStatus,
pub looters: HashSet<String>,
}
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<T: DealerComponent>(&self) -> &HashMap<String, OwnedElement> {
T::get_elements_at(self, &self.location)
}
pub fn get_total_owned_local<T: DealerComponent>(&self) -> usize {
self.get_total_owned_at::<T>(&self.location)
}
pub fn get_total_owned_at<T: DealerComponent>(&self, location: &str) -> usize {
T::get_elements_at(self, &location)
.iter()
.map(|(_, e)| e.amount)
.sum()
}
pub fn get_remaining_capacity_local<T: DealerComponent>(&self) -> usize {
self.get_remaining_capacity_at::<T>(&self.location)
}
pub fn get_remaining_capacity_at<T: DealerComponent>(&self, location: &str) -> usize {
self.capacity - self.get_total_owned_at::<T>(location)
}
pub fn print_status(&self) -> &str {
match self.status {
DealerStatus::Available => "Available",
DealerStatus::Flying => "Flying",
DealerStatus::Dead(_) => "Dead",
}
}
pub fn add_local<T: DealerComponent>(&mut self, name: &str, amount: usize, bought_at: u128) {
let location = self.location.clone();
self.add_at::<T>(&location, name, amount, bought_at)
}
pub fn add_at<T: DealerComponent>(
&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<T: DealerComponent>(&mut self, name: &str, amount: usize) {
let location = self.location.clone();
self.sub_at::<T>(&location, name, amount);
}
pub fn sub_at<T: DealerComponent>(&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>(&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<T: DealerComponent>(
&self,
rng: &mut dyn RngCore,
) -> Option<(String, OwnedElement)> {
let owned_elements = self.get_owned_local::<T>();
let element = owned_elements.iter().choose(rng);
if element.is_none() {
return None;
}
Some((element.unwrap().0.clone(), element.unwrap().1.clone()))
}
}

329
drugwars/src/definitions.rs Normal file
View File

@ -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::<Vec<_>>()
}
}
impl DealerComponent for Drug {
fn get_elements_at<'a>(
dealer: &'a crate::dealer::Dealer,
location: &'a str,
) -> &'a HashMap<String, OwnedElement> {
dealer.owned_drugs.get(location).unwrap()
}
fn get_elements_at_mut<'a>(
dealer: &'a mut crate::dealer::Dealer,
location: &'a str,
) -> &'a mut HashMap<String, OwnedElement> {
dealer.owned_drugs.get_mut(location).unwrap()
}
fn get_market_at<'a>(location: &'a Location) -> &'a HashMap<String, MarketElement> {
&location.drug_market
}
fn get_market_at_mut<'a>(location: &'a mut Location) -> &'a mut HashMap<String, MarketElement> {
&mut location.drug_market
}
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct Weapon {
pub ammo: Option<String>,
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<String, OwnedElement> {
dealer.owned_items.get(location).unwrap()
}
fn get_elements_at_mut<'a>(
dealer: &'a mut crate::dealer::Dealer,
location: &'a str,
) -> &'a mut HashMap<String, OwnedElement> {
dealer.owned_items.get_mut(location).unwrap()
}
fn get_market_at<'a>(location: &'a Location) -> &'a HashMap<String, MarketElement> {
&location.item_market
}
fn get_market_at_mut<'a>(location: &'a mut Location) -> &'a mut HashMap<String, MarketElement> {
&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::<Vec<_>>()
}
}
#[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<String, MarketElement>,
pub item_market: HashMap<String, MarketElement>,
pub messages: Vec<String>,
pub blokes: HashSet<String>,
pub price_mods: Vec<PriceMod>,
pub rumors: Vec<Rumor>,
}
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::<Vec<_>>()
}
}
impl Location {
pub fn get_market<T: DealerComponent>(&self) -> &HashMap<String, MarketElement> {
T::get_market_at(self)
}
pub fn get_market_mut<T: DealerComponent>(&mut self) -> &mut HashMap<String, MarketElement> {
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<bool>,
}

1200
drugwars/src/drug_wars.rs Normal file

File diff suppressed because it is too large Load Diff

135
drugwars/src/error.rs Normal file
View File

@ -0,0 +1,135 @@
use crate::definitions::DealerStatus;
pub type Error = DrugWarsError;
pub type Result<T> = std::result::Result<T, DrugWarsError>;
#[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 {}

342
drugwars/src/main.rs Normal file
View File

@ -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<RwLock<DrugWars>>;
}
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::<Arc<RwLock<DrugWars>>, 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::<Drug>)
.add_system("gi", give::<Item>)
.add_system("bd", buy::<Drug>)
.add_system("sd", sell::<Drug>)
.add_system("bi", buy::<Item>)
.add_system("bc", buy_capacity)
.add_system("si", sell::<Item>)
.add_system("cc", check_capacity_price)
.add_system("cf", check_flight_prices)
.add_system("cshd", check_shipping_prices::<Drug>)
.add_system("cshi", check_shipping_prices::<Item>)
.add_system("shd", ship::<Drug>)
.add_system("shi", ship::<Item>)
.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<String> {
let data = irc.data().get::<GameManager>().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<String> {
let data = irc.data().get::<GameManager>().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<String> {
let data = irc.data().get::<GameManager>().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<String> {
let data = irc.data().get::<GameManager>().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<String> {
let data = irc.data().get::<GameManager>().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<String> {
let data = irc.data().get::<GameManager>().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<String> {
let data = irc.data().get::<GameManager>().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<String> {
let data = irc.data().get::<GameManager>().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<String> {
vec!["melp?".to_owned()]
}
fn explodes(_irc: &mut Irc, _prefix: &IrcPrefix, _arguments: Vec<&str>) -> Vec<String> {
vec!["explodes.".to_owned()]
}
fn ship<T: DealerComponent + Matchable>(
irc: &mut Irc,
prefix: &IrcPrefix,
arguments: Vec<&str>,
) -> Vec<String> {
let data = irc.data().get::<GameManager>().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::<T>(prefix.nick, arguments[0], arguments[1], arguments[2]),
)
}
fn sell<T: DealerComponent + Matchable>(
irc: &mut Irc,
prefix: &IrcPrefix,
arguments: Vec<&str>,
) -> Vec<String> {
let data = irc.data().get::<GameManager>().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::<T>(prefix.nick, arguments[0], arguments[1]),
)
}
fn buy<T: DealerComponent + Matchable>(
irc: &mut Irc,
prefix: &IrcPrefix,
arguments: Vec<&str>,
) -> Vec<String> {
let data = irc.data().get::<GameManager>().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::<T>(prefix.nick, arguments[0], arguments[1]),
)
}
fn check_flight_prices(irc: &mut Irc, prefix: &IrcPrefix, _arguments: Vec<&str>) -> Vec<String> {
let data = irc.data().get::<GameManager>().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<String> {
let data = irc.data().get::<GameManager>().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<String> {
let data = irc.data().get::<GameManager>().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<T: DealerComponent + Matchable>(
irc: &mut Irc,
prefix: &IrcPrefix,
arguments: Vec<&str>,
) -> Vec<String> {
let data = irc.data().get::<GameManager>().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::<T>(prefix.nick, arguments[1], arguments[2], arguments[0]),
)
}
fn buy_capacity(irc: &mut Irc, prefix: &IrcPrefix, arguments: Vec<&str>) -> Vec<String> {
let data = irc.data().get::<GameManager>().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<String> {
let data = irc.data().get::<GameManager>().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<T: DealerComponent + Matchable>(
irc: &mut Irc,
prefix: &IrcPrefix,
arguments: Vec<&str>,
) -> Vec<String> {
let data = irc.data().get::<GameManager>().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::<T>(prefix.nick, arguments[0], arguments[1], arguments[2]),
)
}
fn show_all_dealers(irc: &mut Irc, prefix: &IrcPrefix, _arguments: Vec<&str>) -> Vec<String> {
let data = irc.data().get::<GameManager>().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<String> {
let data = irc.data().get::<GameManager>().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<String> {
let data = irc.data().get::<GameManager>().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<String> {
let data = irc.data().get::<GameManager>().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<String> {
let data = irc.data().get::<GameManager>().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<String> {
let data = irc.data().get::<GameManager>().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<String> {
let data = irc.data().get::<GameManager>().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<String> {
let data = irc.data().get::<GameManager>().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]))
}

690
drugwars/src/render.rs Normal file
View File

@ -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<String> {
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<String> {
vec![self.get_date_and_time()]
}
pub fn render_market(&self, nick: &str, dealer: &Dealer) -> Vec<String> {
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::<Drug>();
let items_owned = dealer.get_owned_local::<Item>();
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::<Drug>()),
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::<Item>()),
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<String> {
let location = self.locations.get(&dealer.location).unwrap();
let mut blokes = location.blokes.iter().collect::<Vec<_>>();
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<String> {
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 <target> <weapon>".to_owned(),
"attack someone".to_owned(),
])
.add_row(["l <target>".to_owned(), "loot a dead player".to_owned()])
.add_row(["lm <money>".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 <amount>".to_owned(),
"buy thugs (cost 10,000 / day)".to_owned(),
])
.add_row(["st <amount>".to_owned(), "sell thugs".to_owned()])
.add_row([
"bd <drug> <amount>".to_owned(),
"buy drug from market".to_owned(),
])
.add_row([
"sd <drug> <amount>".to_owned(),
"sell drug to market".to_owned(),
])
.add_row([
"bi <drug> <amount>".to_owned(),
"buy item from market".to_owned(),
])
.add_row([
"si <drug> <amount>".to_owned(),
"sell item to market".to_owned(),
])
.add_row(["bc <amount>".to_owned(), "buy inventory slots".to_owned()])
.add_row([
"cc <amount>".to_owned(),
"check price to add <amount> inventory slots".to_owned(),
])
.add_row(["cf ".to_owned(), "check flight prices".to_owned()])
.add_row([
"f <destination>".to_owned(),
"fly to destination".to_owned(),
])
.add_row([
"cshd <drug> <amount> <destination>".to_owned(),
"check drug shipping price".to_owned(),
])
.add_row([
"cshi <drug> <amount> <destination>".to_owned(),
"check item shipping price".to_owned(),
])
.add_row([
"shd <drug> <amount> <destination>".to_owned(),
"ship drug to destination".to_owned(),
])
.add_row([
"shi <item> <amount> <destination>".to_owned(),
"ship item to destination".to_owned(),
])
.add_row([
"gm <bloke> <amount>".to_owned(),
"give money to some bloke".to_owned(),
])
.add_row([
"gd <bloke> <drug> <amount>".to_owned(),
"give drugs to some bloke".to_owned(),
])
.add_row([
"gi <bloke> <item> <amount>".to_owned(),
"give items to some bloke".to_owned(),
])
.get()])
.get(),
)
.build()
}
pub fn render_admin_command_list(&self) -> Vec<String> {
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<String> {
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<String> {
let dealers = &self
.dealers
.iter()
.sorted_by_key(|(_, k)| k.laundered_money)
.rev()
.enumerate()
.collect::<Vec<_>>();
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()
}
}

255
drugwars/src/renderer.rs Normal file
View File

@ -0,0 +1,255 @@
use crate::utils::{truncate_string, IrcSafeLen};
pub trait BoxContent {
fn get_lines(&self, width: usize) -> Vec<String>;
fn len(&self) -> usize;
}
pub trait Part {
fn get_lines(&self, width: usize) -> Vec<String>;
}
#[derive(Default, Clone)]
pub struct RenderBoxContent<const N: usize> {
header: Option<[String; N]>,
content: Vec<[String; N]>,
sizes: Option<[usize; N]>,
}
impl<const N: usize> RenderBoxContent<N> {
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<const N: usize> BoxContent for RenderBoxContent<N> {
fn get_lines(&self, width: usize) -> Vec<String> {
//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<String> {
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::<Vec<_>>();
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<String> {
let out = self
.boxes
.iter()
.map(|elem| elem.get_lines(self.width))
.flatten()
.collect::<Vec<_>>();
out
}
}

199
drugwars/src/save.rs Normal file
View File

@ -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<String, Dealer>,
pub locations: HashMap<String, Location>,
pub flights: HashMap<String, String>,
pub shipments: Vec<Shipment>,
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<DrugWars> 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::<Vec<_>>();
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::<Vec<_>>();
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::<Vec<_>>();
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<Self> {
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)
}
}

227
drugwars/src/utils.rs Normal file
View File

@ -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<String, OwnedElement>;
fn get_elements_at_mut<'a>(
dealer: &'a mut Dealer,
location: &'a str,
) -> &'a mut HashMap<String, OwnedElement>;
fn get_market_at<'a>(location: &'a Location) -> &'a HashMap<String, MarketElement>;
fn get_market_at_mut<'a>(location: &'a mut Location) -> &'a mut HashMap<String, MarketElement>;
}
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::<std::result::Result<Vec<&str>, _>>()
.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::<std::result::Result<Vec<&str>, _>>()
.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<u128> {
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<String> {
return vec![format!("{}: {}", nick, message)];
}
pub fn hl_error(nick: &str, message: &str) -> Vec<String> {
return vec![format!("{}: {}", nick, message)];
}
pub fn get_system_output(nick: &str, val: Result<Vec<String>>) -> Vec<String> {
match val {
Ok(output) => output,
Err(err) => vec![format!("{}: {}", nick, err)],
}
}
pub fn column_renderer_single(
width: usize,
header: &str,
content: Vec<Vec<String>>,
) -> Vec<String> {
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.)
}

200
drugwars_config.yaml Normal file
View File

@ -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!

11
irc/Cargo.toml Normal file
View File

@ -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"

193
irc/src/builder.rs Normal file
View File

@ -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<String>,
port: Option<u16>,
ssl: Option<bool>,
channels: Vec<Channel>,
nick: Option<String>,
user: Option<String>,
real: Option<String>,
nickserv_pass: Option<String>,
nickserv_email: Option<String>,
cmdkey: Option<String>,
flood_interval: Option<f32>,
data: TypeMap,
default_system: Option<System>,
systems: HashMap<String, System>,
admin_systems: HashMap<String, System>,
owner: Option<String>,
admins: Vec<String>,
}
impl From<IrcConfig> 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<V: Send + Sync + 'static, T: TypeMapKey + TypeMapKey<Value = V>>(
&mut self,
resource: V,
) -> &mut Self {
self.data.insert::<T>(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()),
}
}
}

43
irc/src/config.rs Normal file
View File

@ -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<String>,
}
#[derive(Deserialize)]
pub(crate) struct IrcConfig {
pub(crate) host: String,
pub(crate) port: u16,
pub(crate) ssl: bool,
pub(crate) channels: Vec<ChannelConfig>,
pub(crate) nick: String,
pub(crate) user: String,
pub(crate) real: String,
pub(crate) nickserv_pass: Option<String>,
pub(crate) nickserv_email: Option<String>,
pub(crate) cmdkey: String,
pub(crate) flood_interval: f32,
pub(crate) owner: String,
pub(crate) admins: Vec<String>,
}
impl IrcConfig {
pub fn from_file(path: &str) -> std::io::Result<Self> {
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)
}
}

67
irc/src/format.rs Normal file
View File

@ -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"),
}
}
}

242
irc/src/irc_command.rs Normal file
View File

@ -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"
);

679
irc/src/lib.rs Normal file
View File

@ -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<String>;
pub enum Stream {
Plain(TcpStream),
Tls(TlsStream<TcpStream>),
}
impl Stream {
pub fn read(&mut self, buf: &mut [u8]) -> std::result::Result<usize, std::io::Error> {
match self {
Stream::Plain(stream) => stream.read(buf),
Stream::Tls(stream) => stream.read(buf),
}
}
pub fn write(&mut self, buf: &[u8]) -> std::result::Result<usize, std::io::Error> {
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<IrcPrefix<'a>>,
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<String>,
}
impl From<ChannelConfig> 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<Stream>,
host: String,
port: u16,
ssl: bool,
channels: Vec<Channel>,
flood_controls: HashMap<String, FloodControl>,
nick: String,
user: String,
real: String,
nickserv_pass: Option<String>,
nickserv_email: Option<String>,
cmdkey: String,
flood_interval: f32,
data: TypeMap,
default_system: Option<System>,
systems: HashMap<String, System>,
admin_systems: HashMap<String, System>,
send_queue: VecDeque<String>,
recv_queue: VecDeque<String>,
owner: String,
admins: Vec<String>,
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::<Vec<_>>().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
}
}

33
irc/src/privmsg.rs Normal file
View File

@ -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
}
}

21
irc_config.example.yaml Normal file
View File

@ -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: []