initial commit
This commit is contained in:
commit
6436bd9f2c
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
@ -0,0 +1,4 @@
|
||||
/target
|
||||
/Cargo.lock
|
||||
save.yaml
|
||||
irc_config.yaml
|
6
Cargo.toml
Normal file
6
Cargo.toml
Normal file
@ -0,0 +1,6 @@
|
||||
[workspace]
|
||||
|
||||
members = [
|
||||
"irc",
|
||||
"drugwars"
|
||||
]
|
3
README.md
Normal file
3
README.md
Normal 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
13
drugwars/Cargo.toml
Normal 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
126
drugwars/src/admin.rs
Normal 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
463
drugwars/src/api.rs
Normal 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
27
drugwars/src/config.rs
Normal 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
212
drugwars/src/dealer.rs
Normal 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
329
drugwars/src/definitions.rs
Normal 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
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
135
drugwars/src/error.rs
Normal 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
342
drugwars/src/main.rs
Normal 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
690
drugwars/src/render.rs
Normal 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
255
drugwars/src/renderer.rs
Normal 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
199
drugwars/src/save.rs
Normal 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
227
drugwars/src/utils.rs
Normal 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
200
drugwars_config.yaml
Normal 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
11
irc/Cargo.toml
Normal 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
193
irc/src/builder.rs
Normal 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
43
irc/src/config.rs
Normal 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
67
irc/src/format.rs
Normal 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
242
irc/src/irc_command.rs
Normal 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
679
irc/src/lib.rs
Normal 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
33
irc/src/privmsg.rs
Normal 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
21
irc_config.example.yaml
Normal 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: []
|
Loading…
Reference in New Issue
Block a user