Compare commits
2 commits
81a9ec0c50
...
d584340f01
| Author | SHA1 | Date | |
|---|---|---|---|
| d584340f01 | |||
| c22787792d |
18 changed files with 1161 additions and 224 deletions
1152
Cargo.lock
generated
1152
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
|
|
@ -2,7 +2,7 @@
|
|||
members = ["cli", "discord"]
|
||||
|
||||
[workspace.package]
|
||||
version = "0.10.0"
|
||||
version = "0.11.0"
|
||||
edition = "2024"
|
||||
repository = "https://2ndbeam.ru/git/2ndbeam/squad-quest"
|
||||
homepage = "https://2ndbeam.ru/git/2ndbeam/squad-quest"
|
||||
|
|
|
|||
|
|
@ -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.10.0", path = ".." }
|
||||
squad-quest = { version = "0.11.0", path = ".." }
|
||||
toml = "0.9.8"
|
||||
|
|
|
|||
|
|
@ -14,6 +14,8 @@ pub enum MapCommands {
|
|||
Delete(MapDeleteArgs),
|
||||
/// Update room data
|
||||
Update(MapUpdateArgs),
|
||||
/// Get room implementation data
|
||||
Data(MapDataArgs),
|
||||
}
|
||||
|
||||
#[derive(Args)]
|
||||
|
|
@ -55,3 +57,9 @@ pub struct MapUpdateArgs {
|
|||
#[arg(short,long)]
|
||||
pub value: Option<u32>,
|
||||
}
|
||||
|
||||
#[derive(Args)]
|
||||
pub struct MapDataArgs {
|
||||
/// Room ID
|
||||
pub id: u16,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -147,7 +147,8 @@ fn main() {
|
|||
answer: args.answer.clone(),
|
||||
public: args.public,
|
||||
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));
|
||||
|
|
@ -169,7 +170,8 @@ 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())
|
||||
deadline: args.deadline.clone().or(quest.deadline.clone()),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
do_and_log(quest.save(path), !cli.quiet, format!("Updated quest #{}.", quest.id));
|
||||
|
|
@ -445,6 +447,15 @@ 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}");
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,7 +10,9 @@ 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"
|
||||
squad-quest = { version = "0.10.0", path = ".." }
|
||||
serde_json = "1.0.146"
|
||||
squad-quest = { version = "0.11.0", path = ".." }
|
||||
tokio = { version = "1.48.0", features = ["rt-multi-thread"] }
|
||||
toml = "0.9.8"
|
||||
|
|
|
|||
8
discord/Rocket.toml
Normal file
8
discord/Rocket.toml
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
[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
|
||||
|
||||
|
|
@ -1,12 +1,24 @@
|
|||
use poise::serenity_prelude::UserId;
|
||||
use std::collections::HashMap;
|
||||
|
||||
use poise::serenity_prelude::{User, UserId};
|
||||
use squad_quest::{account::Account, config::Config, map::Map};
|
||||
|
||||
pub fn fetch_or_init_account(conf: &Config, id: String) -> Account {
|
||||
pub fn fetch_or_init_account(conf: &Config, id: String, user: Option<&User>) -> 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()
|
||||
},
|
||||
}
|
||||
|
|
|
|||
119
discord/src/api.rs
Normal file
119
discord/src/api.rs
Normal file
|
|
@ -0,0 +1,119 @@
|
|||
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})
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -142,7 +142,8 @@ 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);
|
||||
|
||||
let mut user_account = fetch_or_init_account(config, user_id, Some(ctx.author()));
|
||||
|
||||
let who_id = format!("{}", who.id.get());
|
||||
let Some(other_account) = accounts.iter_mut().find(|a| a.id == who_id ) else {
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
let mut account = fetch_or_init_account(&ctx.data().config, ctx.author().id.to_string(), Some(ctx.author()));
|
||||
|
||||
if let Some(_) = account.quests_completed.iter().find(|qid| **qid == quest_id) {
|
||||
return Err(Error::QuestIsCompleted(quest_id));
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
let mut account = fetch_or_init_account(conf, acc_id, Some(ctx.author()));
|
||||
|
||||
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);
|
||||
let mut account = fetch_or_init_account(conf, acc_id, Some(ctx.author()));
|
||||
|
||||
if let None = account.rooms_unlocked.iter().find(|rid| **rid == id) {
|
||||
return Err(Error::CannotReach(id));
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
#[macro_use] extern crate rocket;
|
||||
|
||||
use std::{sync::{Arc, Mutex}};
|
||||
|
||||
use clap::Parser;
|
||||
|
|
@ -5,8 +7,9 @@ use dotenvy::dotenv;
|
|||
use poise::serenity_prelude as serenity;
|
||||
use squad_quest::config::Config;
|
||||
|
||||
use crate::{commands::error_handler, config::{ConfigImpl, DiscordConfig}, error::Error, strings::Strings};
|
||||
use crate::{commands::{error_handler, print_error_recursively}, config::{ConfigImpl, DiscordConfig}, error::Error, strings::Strings};
|
||||
|
||||
mod api;
|
||||
mod commands;
|
||||
mod cli;
|
||||
mod config;
|
||||
|
|
@ -65,6 +68,14 @@ 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)),
|
||||
|
|
@ -87,6 +98,8 @@ 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)),
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
//! User accounts
|
||||
|
||||
use std::{fs, io::Write, path::PathBuf};
|
||||
use std::{collections::HashMap, fs, io::Write, path::PathBuf};
|
||||
|
||||
use serde::{ Serialize, Deserialize };
|
||||
|
||||
|
|
@ -29,6 +29,9 @@ 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 {
|
||||
|
|
@ -39,6 +42,7 @@ impl Default for Account {
|
|||
location: u16::default(),
|
||||
quests_completed: Vec::new(),
|
||||
rooms_unlocked: Vec::new(),
|
||||
data: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)]
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
#[serde(default)]
|
||||
pub struct Config {
|
||||
/// Path to config directory
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
//! Map, a.k.a. a graph of rooms
|
||||
|
||||
use std::{fs, io::Write, path::PathBuf};
|
||||
use std::{collections::HashMap, 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,6 +131,8 @@ 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 {
|
||||
|
|
@ -145,6 +147,7 @@ impl Default for Room {
|
|||
value: u32::default(),
|
||||
name: default_name(),
|
||||
description: None,
|
||||
data: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
//! Text-based quests and user solutions for them
|
||||
|
||||
use std::{fs, io::Write, path::PathBuf};
|
||||
use std::{collections::HashMap, fs, io::Write, path::PathBuf};
|
||||
|
||||
use serde::{ Serialize, Deserialize };
|
||||
use crate::{SquadObject, account::Account, error::{Error, QuestError}};
|
||||
|
|
@ -66,7 +66,10 @@ pub struct Quest {
|
|||
pub available_on: Option<Date>,
|
||||
|
||||
/// 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 {
|
||||
|
|
@ -80,7 +83,8 @@ impl Default for Quest {
|
|||
answer: default_answer(),
|
||||
public: false,
|
||||
available_on: None,
|
||||
deadline: None
|
||||
deadline: None,
|
||||
data: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -38,7 +38,8 @@ 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
|
||||
deadline: None,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
assert_eq!(*quest, expected);
|
||||
|
|
@ -73,7 +74,8 @@ fn account_test() {
|
|||
balance: 150,
|
||||
location: 0,
|
||||
quests_completed: vec![0],
|
||||
rooms_unlocked: Vec::new()
|
||||
rooms_unlocked: Vec::new(),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let accounts = config.load_accounts();
|
||||
|
|
@ -92,6 +94,7 @@ fn load_map() {
|
|||
value: 0,
|
||||
name: "Entrance".to_string(),
|
||||
description: Some("Enter the dungeon".to_string()),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let room1 = Room {
|
||||
|
|
@ -100,6 +103,7 @@ fn load_map() {
|
|||
value: 100,
|
||||
name: "Kitchen hall".to_string(),
|
||||
description: None,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let room2 = Room {
|
||||
|
|
@ -108,6 +112,7 @@ fn load_map() {
|
|||
value: 250,
|
||||
name: "Room".to_string(),
|
||||
description: Some("Simple room with no furniture".to_string()),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let room3 = Room {
|
||||
|
|
@ -116,6 +121,7 @@ fn load_map() {
|
|||
value: 175,
|
||||
name: "Kitchen".to_string(),
|
||||
description: Some("Knives are stored here".to_string()),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let expected = Map {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue