Compare commits

..

No commits in common. "d584340f010e67919de8be2fffde17ffd9f20cdb" and "81a9ec0c50468cd6cdd4192323b6303bf7dd32d6" have entirely different histories.

18 changed files with 216 additions and 1153 deletions

1136
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -2,7 +2,7 @@
members = ["cli", "discord"] members = ["cli", "discord"]
[workspace.package] [workspace.package]
version = "0.11.0" version = "0.10.0"
edition = "2024" edition = "2024"
repository = "https://2ndbeam.ru/git/2ndbeam/squad-quest" repository = "https://2ndbeam.ru/git/2ndbeam/squad-quest"
homepage = "https://2ndbeam.ru/git/2ndbeam/squad-quest" homepage = "https://2ndbeam.ru/git/2ndbeam/squad-quest"

View file

@ -9,5 +9,5 @@ license.workspace = true
chrono = "0.4.42" chrono = "0.4.42"
clap = { version = "4.5.53", features = ["derive"] } clap = { version = "4.5.53", features = ["derive"] }
serde = { version = "1.0.228", features = ["derive"] } serde = { version = "1.0.228", features = ["derive"] }
squad-quest = { version = "0.11.0", path = ".." } squad-quest = { version = "0.10.0", path = ".." }
toml = "0.9.8" toml = "0.9.8"

View file

@ -14,8 +14,6 @@ pub enum MapCommands {
Delete(MapDeleteArgs), Delete(MapDeleteArgs),
/// Update room data /// Update room data
Update(MapUpdateArgs), Update(MapUpdateArgs),
/// Get room implementation data
Data(MapDataArgs),
} }
#[derive(Args)] #[derive(Args)]
@ -57,9 +55,3 @@ pub struct MapUpdateArgs {
#[arg(short,long)] #[arg(short,long)]
pub value: Option<u32>, pub value: Option<u32>,
} }
#[derive(Args)]
pub struct MapDataArgs {
/// Room ID
pub id: u16,
}

View file

@ -147,8 +147,7 @@ fn main() {
answer: args.answer.clone(), answer: args.answer.clone(),
public: args.public, public: args.public,
available_on: args.available.clone(), available_on: args.available.clone(),
deadline: args.deadline.clone(), deadline: args.deadline.clone()
..Default::default()
}; };
do_and_log(quest.save(path), !cli.quiet, format!("Created quest #{}.", quest.id)); do_and_log(quest.save(path), !cli.quiet, format!("Created quest #{}.", quest.id));
@ -170,8 +169,7 @@ fn main() {
answer: args.answer.clone().unwrap_or(quest.answer.clone()), answer: args.answer.clone().unwrap_or(quest.answer.clone()),
public: args.public.unwrap_or(quest.public), public: args.public.unwrap_or(quest.public),
available_on: args.available.clone().or(quest.available_on.clone()), available_on: args.available.clone().or(quest.available_on.clone()),
deadline: args.deadline.clone().or(quest.deadline.clone()), deadline: args.deadline.clone().or(quest.deadline.clone())
..Default::default()
}; };
do_and_log(quest.save(path), !cli.quiet, format!("Updated quest #{}.", quest.id)); do_and_log(quest.save(path), !cli.quiet, format!("Updated quest #{}.", quest.id));
@ -447,15 +445,6 @@ fn main() {
let connected = if connect { "Connected" } else { "Disconnected" }; let connected = if connect { "Connected" } else { "Disconnected" };
do_and_log(map_save(map, map_path), !cli.quiet, format!("{connected} rooms #{} <-> #{}.", args.first, args.second)); do_and_log(map_save(map, map_path), !cli.quiet, format!("{connected} rooms #{} <-> #{}.", args.first, args.second));
}, },
MapCommands::Data(args) => {
if let Some(room) = map.room.iter().find(|r| r.id == args.id) {
if let Some(data) = &room.data {
for (key, value) in data {
println!("{key} = {value}");
}
}
}
},
} }
} }
} }

View file

@ -10,9 +10,7 @@ chrono = "0.4.42"
clap = { version = "4.5.53", features = ["derive"] } clap = { version = "4.5.53", features = ["derive"] }
dotenvy = "0.15.7" dotenvy = "0.15.7"
poise = "0.6.1" poise = "0.6.1"
rocket = { version = "0.5.1", features = ["json"] }
serde = "1.0.228" serde = "1.0.228"
serde_json = "1.0.146" squad-quest = { version = "0.10.0", path = ".." }
squad-quest = { version = "0.11.0", path = ".." }
tokio = { version = "1.48.0", features = ["rt-multi-thread"] } tokio = { version = "1.48.0", features = ["rt-multi-thread"] }
toml = "0.9.8" toml = "0.9.8"

View file

@ -1,8 +0,0 @@
[default]
address = "127.0.0.1" # should be local only because frontend runs on the same machine
port = 2526
log_level = "critical"
[default.shutdown]
ctrlc = false

View file

@ -1,24 +1,12 @@
use std::collections::HashMap; use poise::serenity_prelude::UserId;
use poise::serenity_prelude::{User, UserId};
use squad_quest::{account::Account, config::Config, map::Map}; use squad_quest::{account::Account, config::Config, map::Map};
pub fn fetch_or_init_account(conf: &Config, id: String, user: Option<&User>) -> Account { pub fn fetch_or_init_account(conf: &Config, id: String) -> Account {
let accounts = conf.load_accounts(); let accounts = conf.load_accounts();
let mut data: HashMap<String, String> = HashMap::new();
if let Some(user) = user {
let avatar = user.avatar_url().unwrap_or("null".to_string());
let name = user.display_name().to_string();
data.insert("avatar".to_string(), avatar);
data.insert("name".to_string(), name);
}
match accounts.iter().find(|a| a.id == id) { match accounts.iter().find(|a| a.id == id) {
Some(a) => a.clone(), Some(a) => a.clone(),
None => Account { None => Account {
id, id,
data: Some(data),
..Default::default() ..Default::default()
}, },
} }

View file

@ -1,119 +0,0 @@
use rocket::{Build, Response, Rocket, State, http::{Header, hyper::header::ACCESS_CONTROL_ALLOW_ORIGIN}, response::Responder, serde::json::Json};
use serde::Serialize;
use squad_quest::{SquadObject, account::Account, config::Config, map::{Map, Room}};
struct RocketData {
pub config: Config,
}
#[derive(Serialize)]
struct UserData {
pub id: String,
pub avatar: String,
pub name: String,
}
#[derive(Serialize)]
struct RoomData {
pub id: u16,
pub value: u32,
pub name: String,
pub description: String,
pub x: f32,
pub y: f32,
pub w: f32,
pub h: f32,
pub markers: Vec<UserData>,
}
struct RoomDataResponse {
pub data: Vec<RoomData>
}
impl From<Vec<RoomData>> for RoomDataResponse {
fn from(value: Vec<RoomData>) -> Self {
Self {
data: value,
}
}
}
impl<'r> Responder<'r, 'static> for RoomDataResponse {
fn respond_to(self, request: &'r rocket::Request<'_>) -> rocket::response::Result<'static> {
Response::build_from(Json(&self.data).respond_to(request)?)
.header(Header::new(ACCESS_CONTROL_ALLOW_ORIGIN.as_str(), "http://localhost:5173"))
.ok()
}
}
impl From<&Room> for RoomData {
fn from(value: &Room) -> Self {
let data = value.data.clone().unwrap_or_default();
let keys = [ "x", "y", "w", "h" ];
let mut values = [ 0f32, 0f32, 0f32, 0f32 ];
let mut counter = 0usize;
for key in keys {
values[counter] = data.get(key).map_or(0f32, |v| v.parse::<f32>().unwrap_or_default());
counter += 1;
}
RoomData {
id: value.id,
value: value.value,
name: value.name.clone(),
description: value.description.clone().unwrap_or(String::new()),
x: values[0],
y: values[1],
w: values[2],
h: values[3],
markers: Vec::new(),
}
}
}
fn acc_filt_map(account: &Account, room_id: u16) -> Option<UserData> {
if account.location == room_id {
let data = account.data.clone().unwrap_or_default();
let keys = [ "avatar", "name" ];
let empty = String::new();
let mut values = [ &String::new(), &String::new() ];
let mut counter = 0usize;
for key in keys {
values[counter] = data.get(key).unwrap_or(&empty);
counter += 1;
}
Some(UserData {
id: account.id.clone(),
avatar: values[0].clone(),
name: values[1].clone(),
})
} else { None }
}
#[get("/")]
fn index(rd: &State<RocketData>) -> RoomDataResponse {
let map_path = rd.config.full_map_path();
let Ok(map) = Map::load(map_path) else {
return Vec::new().into();
};
let accounts = rd.config.load_accounts();
let rooms_vec: Vec<RoomData> = map.room.iter()
.map(|r| {
let mut rd = RoomData::from(r);
let markers = accounts.iter()
.filter_map(|a| acc_filt_map(a, r.id))
.collect::<Vec<UserData>>();
rd.markers = markers;
rd
})
.collect();
rooms_vec.into()
}
pub fn rocket(config: Config) -> Rocket<Build> {
rocket::build()
.mount("/", routes![index])
.manage(RocketData{config})
}

View file

@ -142,8 +142,7 @@ pub async fn give(
let mut accounts = config.load_accounts(); let mut accounts = config.load_accounts();
let user_id = format!("{}", ctx.author().id.get()); let user_id = format!("{}", ctx.author().id.get());
let mut user_account = fetch_or_init_account(config, user_id);
let mut user_account = fetch_or_init_account(config, user_id, Some(ctx.author()));
let who_id = format!("{}", who.id.get()); let who_id = format!("{}", who.id.get());
let Some(other_account) = accounts.iter_mut().find(|a| a.id == who_id ) else { let Some(other_account) = accounts.iter_mut().find(|a| a.id == who_id ) else {

View file

@ -34,7 +34,7 @@ pub async fn answer(
#[description_localized("ru", "Вложение к ответу на квест")] #[description_localized("ru", "Вложение к ответу на квест")]
file3: Option<Attachment>, file3: Option<Attachment>,
) -> Result<(), Error> { ) -> Result<(), Error> {
let mut account = fetch_or_init_account(&ctx.data().config, ctx.author().id.to_string(), Some(ctx.author())); let mut account = fetch_or_init_account(&ctx.data().config, ctx.author().id.to_string());
if let Some(_) = account.quests_completed.iter().find(|qid| **qid == quest_id) { if let Some(_) = account.quests_completed.iter().find(|qid| **qid == quest_id) {
return Err(Error::QuestIsCompleted(quest_id)); return Err(Error::QuestIsCompleted(quest_id));

View file

@ -27,7 +27,7 @@ pub async fn unlock(
}; };
let acc_id = format!("{}", ctx.author().id.get()); let acc_id = format!("{}", ctx.author().id.get());
let mut account = fetch_or_init_account(conf, acc_id, Some(ctx.author())); let mut account = fetch_or_init_account(conf, acc_id);
if account.balance < room.value { if account.balance < room.value {
return Err(Error::InsufficientFunds(room.value)); return Err(Error::InsufficientFunds(room.value));
@ -68,7 +68,7 @@ pub async fn r#move(
let conf = &ctx.data().config; let conf = &ctx.data().config;
let acc_id = format!("{}", ctx.author().id.get()); let acc_id = format!("{}", ctx.author().id.get());
let mut account = fetch_or_init_account(conf, acc_id, Some(ctx.author())); let mut account = fetch_or_init_account(conf, acc_id);
if let None = account.rooms_unlocked.iter().find(|rid| **rid == id) { if let None = account.rooms_unlocked.iter().find(|rid| **rid == id) {
return Err(Error::CannotReach(id)); return Err(Error::CannotReach(id));

View file

@ -1,5 +1,3 @@
#[macro_use] extern crate rocket;
use std::{sync::{Arc, Mutex}}; use std::{sync::{Arc, Mutex}};
use clap::Parser; use clap::Parser;
@ -7,9 +5,8 @@ use dotenvy::dotenv;
use poise::serenity_prelude as serenity; use poise::serenity_prelude as serenity;
use squad_quest::config::Config; use squad_quest::config::Config;
use crate::{commands::{error_handler, print_error_recursively}, config::{ConfigImpl, DiscordConfig}, error::Error, strings::Strings}; use crate::{commands::error_handler, config::{ConfigImpl, DiscordConfig}, error::Error, strings::Strings};
mod api;
mod commands; mod commands;
mod cli; mod cli;
mod config; mod config;
@ -68,14 +65,6 @@ async fn main() {
let token = std::env::var(DISCORD_TOKEN).expect("missing DISCORD_TOKEN"); let token = std::env::var(DISCORD_TOKEN).expect("missing DISCORD_TOKEN");
let intents = serenity::GatewayIntents::non_privileged(); let intents = serenity::GatewayIntents::non_privileged();
let conf1 = config.clone();
tokio::spawn(async {
if let Err(error) = api::rocket(conf1).launch().await {
eprintln!("ERROR ON API LAUNCH");
print_error_recursively(&error);
}
});
let framework = poise::Framework::builder() let framework = poise::Framework::builder()
.options(poise::FrameworkOptions { .options(poise::FrameworkOptions {
on_error: |err| Box::pin(error_handler(err)), on_error: |err| Box::pin(error_handler(err)),
@ -98,8 +87,6 @@ async fn main() {
.setup(|_ctx, _ready, _framework| { .setup(|_ctx, _ready, _framework| {
Box::pin(async move { Box::pin(async move {
//poise::builtins::register_globally(ctx, &framework.options().commands).await?; //poise::builtins::register_globally(ctx, &framework.options().commands).await?;
Ok(Data { Ok(Data {
config, config,
discord: Arc::new(Mutex::new(discord)), discord: Arc::new(Mutex::new(discord)),

View file

@ -1,6 +1,6 @@
//! User accounts //! User accounts
use std::{collections::HashMap, fs, io::Write, path::PathBuf}; use std::{fs, io::Write, path::PathBuf};
use serde::{ Serialize, Deserialize }; use serde::{ Serialize, Deserialize };
@ -29,9 +29,6 @@ pub struct Account {
/// Vec of rooms unlocked by this user /// Vec of rooms unlocked by this user
pub rooms_unlocked: Vec<u16>, pub rooms_unlocked: Vec<u16>,
/// Additional implementation-defined data
pub data: Option<HashMap<String, String>>,
} }
impl Default for Account { impl Default for Account {
@ -42,7 +39,6 @@ impl Default for Account {
location: u16::default(), location: u16::default(),
quests_completed: Vec::new(), quests_completed: Vec::new(),
rooms_unlocked: Vec::new(), rooms_unlocked: Vec::new(),
data: None,
} }
} }
} }

View file

@ -6,7 +6,7 @@ use serde::{Deserialize, Serialize};
use crate::{SquadObject, account::Account, error::Error, quest::Quest}; use crate::{SquadObject, account::Account, error::Error, quest::Quest};
/// Struct for containing paths to other (de-)serializable things /// Struct for containing paths to other (de-)serializable things
#[derive(Serialize, Deserialize, Debug, Clone)] #[derive(Serialize, Deserialize, Debug)]
#[serde(default)] #[serde(default)]
pub struct Config { pub struct Config {
/// Path to config directory /// Path to config directory

View file

@ -1,6 +1,6 @@
//! Map, a.k.a. a graph of rooms //! Map, a.k.a. a graph of rooms
use std::{collections::HashMap, fs, io::Write, path::PathBuf}; use std::{fs, io::Write, path::PathBuf};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
@ -11,7 +11,7 @@ use crate::{SquadObject, account::Account, error::{Error, MapError}};
#[serde(default)] #[serde(default)]
pub struct Map { pub struct Map {
/// Rooms go here /// Rooms go here
pub room: Vec<Room>, pub room: Vec<Room>
} }
impl Default for Map { impl Default for Map {
@ -131,8 +131,6 @@ pub struct Room {
pub name: String, pub name: String,
/// Room description /// Room description
pub description: Option<String>, pub description: Option<String>,
/// Additional implementation-based data
pub data: Option<HashMap<String, String>>,
} }
fn default_name() -> String { fn default_name() -> String {
@ -147,7 +145,6 @@ impl Default for Room {
value: u32::default(), value: u32::default(),
name: default_name(), name: default_name(),
description: None, description: None,
data: None,
} }
} }
} }

View file

@ -1,6 +1,6 @@
//! Text-based quests and user solutions for them //! Text-based quests and user solutions for them
use std::{collections::HashMap, fs, io::Write, path::PathBuf}; use std::{fs, io::Write, path::PathBuf};
use serde::{ Serialize, Deserialize }; use serde::{ Serialize, Deserialize };
use crate::{SquadObject, account::Account, error::{Error, QuestError}}; use crate::{SquadObject, account::Account, error::{Error, QuestError}};
@ -66,10 +66,7 @@ pub struct Quest {
pub available_on: Option<Date>, pub available_on: Option<Date>,
/// When quest expires /// When quest expires
pub deadline: Option<Date>, pub deadline: Option<Date>
/// Additional implementation-defined data
pub data: Option<HashMap<String, String>>,
} }
impl Default for Quest { impl Default for Quest {
@ -83,8 +80,7 @@ impl Default for Quest {
answer: default_answer(), answer: default_answer(),
public: false, public: false,
available_on: None, available_on: None,
deadline: None, deadline: None
data: None,
} }
} }
} }

View file

@ -38,8 +38,7 @@ fn quest_one() {
answer: "Accept the answer if it has no attachments and an empty comment".to_owned(), answer: "Accept the answer if it has no attachments and an empty comment".to_owned(),
public: false, public: false,
available_on: None, available_on: None,
deadline: None, deadline: None
..Default::default()
}; };
assert_eq!(*quest, expected); assert_eq!(*quest, expected);
@ -74,8 +73,7 @@ fn account_test() {
balance: 150, balance: 150,
location: 0, location: 0,
quests_completed: vec![0], quests_completed: vec![0],
rooms_unlocked: Vec::new(), rooms_unlocked: Vec::new()
..Default::default()
}; };
let accounts = config.load_accounts(); let accounts = config.load_accounts();
@ -94,7 +92,6 @@ fn load_map() {
value: 0, value: 0,
name: "Entrance".to_string(), name: "Entrance".to_string(),
description: Some("Enter the dungeon".to_string()), description: Some("Enter the dungeon".to_string()),
..Default::default()
}; };
let room1 = Room { let room1 = Room {
@ -103,7 +100,6 @@ fn load_map() {
value: 100, value: 100,
name: "Kitchen hall".to_string(), name: "Kitchen hall".to_string(),
description: None, description: None,
..Default::default()
}; };
let room2 = Room { let room2 = Room {
@ -112,7 +108,6 @@ fn load_map() {
value: 250, value: 250,
name: "Room".to_string(), name: "Room".to_string(),
description: Some("Simple room with no furniture".to_string()), description: Some("Simple room with no furniture".to_string()),
..Default::default()
}; };
let room3 = Room { let room3 = Room {
@ -121,7 +116,6 @@ fn load_map() {
value: 175, value: 175,
name: "Kitchen".to_string(), name: "Kitchen".to_string(),
description: Some("Knives are stored here".to_string()), description: Some("Knives are stored here".to_string()),
..Default::default()
}; };
let expected = Map { let expected = Map {