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"]
[workspace.package]
version = "0.11.0"
version = "0.10.0"
edition = "2024"
repository = "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"
clap = { version = "4.5.53", 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"

View file

@ -14,8 +14,6 @@ pub enum MapCommands {
Delete(MapDeleteArgs),
/// Update room data
Update(MapUpdateArgs),
/// Get room implementation data
Data(MapDataArgs),
}
#[derive(Args)]
@ -57,9 +55,3 @@ pub struct MapUpdateArgs {
#[arg(short,long)]
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(),
public: args.public,
available_on: args.available.clone(),
deadline: args.deadline.clone(),
..Default::default()
deadline: args.deadline.clone()
};
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()),
public: args.public.unwrap_or(quest.public),
available_on: args.available.clone().or(quest.available_on.clone()),
deadline: args.deadline.clone().or(quest.deadline.clone()),
..Default::default()
deadline: args.deadline.clone().or(quest.deadline.clone())
};
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" };
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"] }
dotenvy = "0.15.7"
poise = "0.6.1"
rocket = { version = "0.5.1", features = ["json"] }
serde = "1.0.228"
serde_json = "1.0.146"
squad-quest = { version = "0.11.0", path = ".." }
squad-quest = { version = "0.10.0", path = ".." }
tokio = { version = "1.48.0", features = ["rt-multi-thread"] }
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::{User, UserId};
use poise::serenity_prelude::UserId;
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 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) {
Some(a) => a.clone(),
None => Account {
id,
data: Some(data),
..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 user_id = format!("{}", ctx.author().id.get());
let mut user_account = fetch_or_init_account(config, user_id, Some(ctx.author()));
let mut user_account = fetch_or_init_account(config, user_id);
let who_id = format!("{}", who.id.get());
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", "Вложение к ответу на квест")]
file3: Option<Attachment>,
) -> 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) {
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 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 {
return Err(Error::InsufficientFunds(room.value));
@ -68,7 +68,7 @@ pub async fn r#move(
let conf = &ctx.data().config;
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) {
return Err(Error::CannotReach(id));

View file

@ -1,5 +1,3 @@
#[macro_use] extern crate rocket;
use std::{sync::{Arc, Mutex}};
use clap::Parser;
@ -7,9 +5,8 @@ use dotenvy::dotenv;
use poise::serenity_prelude as serenity;
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 cli;
mod config;
@ -68,14 +65,6 @@ async fn main() {
let token = std::env::var(DISCORD_TOKEN).expect("missing DISCORD_TOKEN");
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()
.options(poise::FrameworkOptions {
on_error: |err| Box::pin(error_handler(err)),
@ -98,8 +87,6 @@ async fn main() {
.setup(|_ctx, _ready, _framework| {
Box::pin(async move {
//poise::builtins::register_globally(ctx, &framework.options().commands).await?;
Ok(Data {
config,
discord: Arc::new(Mutex::new(discord)),

View file

@ -1,6 +1,6 @@
//! User accounts
use std::{collections::HashMap, fs, io::Write, path::PathBuf};
use std::{fs, io::Write, path::PathBuf};
use serde::{ Serialize, Deserialize };
@ -29,9 +29,6 @@ pub struct Account {
/// Vec of rooms unlocked by this user
pub rooms_unlocked: Vec<u16>,
/// Additional implementation-defined data
pub data: Option<HashMap<String, String>>,
}
impl Default for Account {
@ -42,7 +39,6 @@ impl Default for Account {
location: u16::default(),
quests_completed: 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};
/// Struct for containing paths to other (de-)serializable things
#[derive(Serialize, Deserialize, Debug, Clone)]
#[derive(Serialize, Deserialize, Debug)]
#[serde(default)]
pub struct Config {
/// Path to config directory

View file

@ -1,6 +1,6 @@
//! 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};
@ -11,7 +11,7 @@ use crate::{SquadObject, account::Account, error::{Error, MapError}};
#[serde(default)]
pub struct Map {
/// Rooms go here
pub room: Vec<Room>,
pub room: Vec<Room>
}
impl Default for Map {
@ -131,8 +131,6 @@ pub struct Room {
pub name: String,
/// Room description
pub description: Option<String>,
/// Additional implementation-based data
pub data: Option<HashMap<String, String>>,
}
fn default_name() -> String {
@ -147,7 +145,6 @@ impl Default for Room {
value: u32::default(),
name: default_name(),
description: None,
data: None,
}
}
}

View file

@ -1,6 +1,6 @@
//! 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 crate::{SquadObject, account::Account, error::{Error, QuestError}};
@ -66,10 +66,7 @@ pub struct Quest {
pub available_on: Option<Date>,
/// When quest expires
pub deadline: Option<Date>,
/// Additional implementation-defined data
pub data: Option<HashMap<String, String>>,
pub deadline: Option<Date>
}
impl Default for Quest {
@ -83,8 +80,7 @@ impl Default for Quest {
answer: default_answer(),
public: false,
available_on: None,
deadline: None,
data: None,
deadline: 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(),
public: false,
available_on: None,
deadline: None,
..Default::default()
deadline: None
};
assert_eq!(*quest, expected);
@ -74,8 +73,7 @@ fn account_test() {
balance: 150,
location: 0,
quests_completed: vec![0],
rooms_unlocked: Vec::new(),
..Default::default()
rooms_unlocked: Vec::new()
};
let accounts = config.load_accounts();
@ -94,7 +92,6 @@ fn load_map() {
value: 0,
name: "Entrance".to_string(),
description: Some("Enter the dungeon".to_string()),
..Default::default()
};
let room1 = Room {
@ -103,7 +100,6 @@ fn load_map() {
value: 100,
name: "Kitchen hall".to_string(),
description: None,
..Default::default()
};
let room2 = Room {
@ -112,7 +108,6 @@ fn load_map() {
value: 250,
name: "Room".to_string(),
description: Some("Simple room with no furniture".to_string()),
..Default::default()
};
let room3 = Room {
@ -121,7 +116,6 @@ fn load_map() {
value: 175,
name: "Kitchen".to_string(),
description: Some("Knives are stored here".to_string()),
..Default::default()
};
let expected = Map {