Compare commits
11 commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 2960b6dfc4 | |||
| f88e010b4f | |||
| 790fa88fe3 | |||
| 47f55105dd | |||
| 5d6aa0422d | |||
| b9f75e426c | |||
| dc94f2060c | |||
| 0e8cdde697 | |||
| 1dc7d94785 | |||
| a0bec4003c | |||
| 78da6dde05 |
27 changed files with 1807 additions and 372 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -1 +1,2 @@
|
||||||
/target
|
/target
|
||||||
|
/cli/target
|
||||||
|
|
|
||||||
248
Cargo.lock
generated
248
Cargo.lock
generated
|
|
@ -2,6 +2,15 @@
|
||||||
# It is not intended for manual editing.
|
# It is not intended for manual editing.
|
||||||
version = 4
|
version = 4
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "android_system_properties"
|
||||||
|
version = "0.1.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "anstream"
|
name = "anstream"
|
||||||
version = "0.6.21"
|
version = "0.6.21"
|
||||||
|
|
@ -52,6 +61,47 @@ dependencies = [
|
||||||
"windows-sys",
|
"windows-sys",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "autocfg"
|
||||||
|
version = "1.5.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "bumpalo"
|
||||||
|
version = "3.19.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cc"
|
||||||
|
version = "1.2.48"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c481bdbf0ed3b892f6f806287d72acd515b352a4ec27a208489b8c1bc839633a"
|
||||||
|
dependencies = [
|
||||||
|
"find-msvc-tools",
|
||||||
|
"shlex",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cfg-if"
|
||||||
|
version = "1.0.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "chrono"
|
||||||
|
version = "0.4.42"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2"
|
||||||
|
dependencies = [
|
||||||
|
"iana-time-zone",
|
||||||
|
"js-sys",
|
||||||
|
"num-traits",
|
||||||
|
"wasm-bindgen",
|
||||||
|
"windows-link",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "clap"
|
name = "clap"
|
||||||
version = "4.5.53"
|
version = "4.5.53"
|
||||||
|
|
@ -98,12 +148,24 @@ version = "1.0.4"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75"
|
checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "core-foundation-sys"
|
||||||
|
version = "0.8.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "equivalent"
|
name = "equivalent"
|
||||||
version = "1.0.2"
|
version = "1.0.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
|
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "find-msvc-tools"
|
||||||
|
version = "0.1.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3a3076410a55c90011c298b04d0cfa770b00fa04e1e3c97d3f6c9de105a03844"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hashbrown"
|
name = "hashbrown"
|
||||||
version = "0.16.1"
|
version = "0.16.1"
|
||||||
|
|
@ -116,6 +178,30 @@ version = "0.5.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
|
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "iana-time-zone"
|
||||||
|
version = "0.1.64"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "33e57f83510bb73707521ebaffa789ec8caf86f9657cad665b092b581d40e9fb"
|
||||||
|
dependencies = [
|
||||||
|
"android_system_properties",
|
||||||
|
"core-foundation-sys",
|
||||||
|
"iana-time-zone-haiku",
|
||||||
|
"js-sys",
|
||||||
|
"log",
|
||||||
|
"wasm-bindgen",
|
||||||
|
"windows-core",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "iana-time-zone-haiku"
|
||||||
|
version = "0.1.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f"
|
||||||
|
dependencies = [
|
||||||
|
"cc",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "indexmap"
|
name = "indexmap"
|
||||||
version = "2.12.1"
|
version = "2.12.1"
|
||||||
|
|
@ -132,6 +218,43 @@ version = "1.70.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695"
|
checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "js-sys"
|
||||||
|
version = "0.3.83"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "464a3709c7f55f1f721e5389aa6ea4e3bc6aba669353300af094b29ffbdde1d8"
|
||||||
|
dependencies = [
|
||||||
|
"once_cell",
|
||||||
|
"wasm-bindgen",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "libc"
|
||||||
|
version = "0.2.177"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "log"
|
||||||
|
version = "0.4.28"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "num-traits"
|
||||||
|
version = "0.2.19"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
|
||||||
|
dependencies = [
|
||||||
|
"autocfg",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "once_cell"
|
||||||
|
version = "1.21.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "once_cell_polyfill"
|
name = "once_cell_polyfill"
|
||||||
version = "1.70.2"
|
version = "1.70.2"
|
||||||
|
|
@ -156,6 +279,12 @@ dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rustversion"
|
||||||
|
version = "1.0.22"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde"
|
name = "serde"
|
||||||
version = "1.0.228"
|
version = "1.0.228"
|
||||||
|
|
@ -195,16 +324,31 @@ dependencies = [
|
||||||
"serde_core",
|
"serde_core",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "shlex"
|
||||||
|
version = "1.3.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "squad-quest"
|
name = "squad-quest"
|
||||||
version = "0.1.0"
|
version = "0.5.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"clap",
|
|
||||||
"clap_derive",
|
|
||||||
"serde",
|
"serde",
|
||||||
"toml",
|
"toml",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "squad-quest-cli"
|
||||||
|
version = "0.5.0"
|
||||||
|
dependencies = [
|
||||||
|
"chrono",
|
||||||
|
"clap",
|
||||||
|
"serde",
|
||||||
|
"squad-quest",
|
||||||
|
"toml",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "strsim"
|
name = "strsim"
|
||||||
version = "0.11.1"
|
version = "0.11.1"
|
||||||
|
|
@ -273,12 +417,110 @@ version = "0.2.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
|
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wasm-bindgen"
|
||||||
|
version = "0.2.106"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0d759f433fa64a2d763d1340820e46e111a7a5ab75f993d1852d70b03dbb80fd"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"once_cell",
|
||||||
|
"rustversion",
|
||||||
|
"wasm-bindgen-macro",
|
||||||
|
"wasm-bindgen-shared",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wasm-bindgen-macro"
|
||||||
|
version = "0.2.106"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "48cb0d2638f8baedbc542ed444afc0644a29166f1595371af4fecf8ce1e7eeb3"
|
||||||
|
dependencies = [
|
||||||
|
"quote",
|
||||||
|
"wasm-bindgen-macro-support",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wasm-bindgen-macro-support"
|
||||||
|
version = "0.2.106"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "cefb59d5cd5f92d9dcf80e4683949f15ca4b511f4ac0a6e14d4e1ac60c6ecd40"
|
||||||
|
dependencies = [
|
||||||
|
"bumpalo",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
"wasm-bindgen-shared",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wasm-bindgen-shared"
|
||||||
|
version = "0.2.106"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "cbc538057e648b67f72a982e708d485b2efa771e1ac05fec311f9f63e5800db4"
|
||||||
|
dependencies = [
|
||||||
|
"unicode-ident",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-core"
|
||||||
|
version = "0.62.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb"
|
||||||
|
dependencies = [
|
||||||
|
"windows-implement",
|
||||||
|
"windows-interface",
|
||||||
|
"windows-link",
|
||||||
|
"windows-result",
|
||||||
|
"windows-strings",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-implement"
|
||||||
|
version = "0.60.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-interface"
|
||||||
|
version = "0.59.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows-link"
|
name = "windows-link"
|
||||||
version = "0.2.1"
|
version = "0.2.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
|
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-result"
|
||||||
|
version = "0.4.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5"
|
||||||
|
dependencies = [
|
||||||
|
"windows-link",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-strings"
|
||||||
|
version = "0.5.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091"
|
||||||
|
dependencies = [
|
||||||
|
"windows-link",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows-sys"
|
name = "windows-sys"
|
||||||
version = "0.61.2"
|
version = "0.61.2"
|
||||||
|
|
|
||||||
17
Cargo.toml
17
Cargo.toml
|
|
@ -1,10 +1,19 @@
|
||||||
|
[workspace]
|
||||||
|
members = ["cli"]
|
||||||
|
|
||||||
|
[workspace.package]
|
||||||
|
version = "0.5.1"
|
||||||
|
edition = "2024"
|
||||||
|
repository = "https://2ndbeam.ru/git/2ndbeam/squad-quest"
|
||||||
|
license = "MIT"
|
||||||
|
|
||||||
[package]
|
[package]
|
||||||
name = "squad-quest"
|
name = "squad-quest"
|
||||||
version = "0.1.0"
|
edition.workspace = true
|
||||||
edition = "2024"
|
version.workspace = true
|
||||||
|
repository.workspace = true
|
||||||
|
license.workspace = true
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
clap = { version = "4.5.53", features = ["derive"] }
|
|
||||||
clap_derive = "4.5.49"
|
|
||||||
serde = { version = "1.0.228", features = ["derive"] }
|
serde = { version = "1.0.228", features = ["derive"] }
|
||||||
toml = "0.9.8"
|
toml = "0.9.8"
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,4 @@
|
||||||
# Default config
|
quests_path = "quests"
|
||||||
|
accounts_path = "accounts"
|
||||||
# Path to quests folder relative to config
|
map = "map.toml"
|
||||||
quests_path = "./quests"
|
verbose = true
|
||||||
|
|
||||||
# Path to accounts folder relative to config
|
|
||||||
accounts_path = "./accounts"
|
|
||||||
|
|
||||||
# Path to map .toml file relative to config
|
|
||||||
map = "./map.toml"
|
|
||||||
|
|
|
||||||
13
cli/Cargo.toml
Normal file
13
cli/Cargo.toml
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
[package]
|
||||||
|
name = "squad-quest-cli"
|
||||||
|
version.workspace = true
|
||||||
|
edition.workspace = true
|
||||||
|
repository.workspace = true
|
||||||
|
license.workspace = true
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
chrono = "0.4.42"
|
||||||
|
clap = { version = "4.5.53", features = ["derive"] }
|
||||||
|
serde = { version = "1.0.228", features = ["derive"] }
|
||||||
|
squad-quest = { version = "0.5.0", path = ".." }
|
||||||
|
toml = "0.9.8"
|
||||||
66
cli/src/cli/account.rs
Normal file
66
cli/src/cli/account.rs
Normal file
|
|
@ -0,0 +1,66 @@
|
||||||
|
use clap::{Args,Subcommand,ValueEnum};
|
||||||
|
|
||||||
|
#[derive(Subcommand)]
|
||||||
|
pub enum AccountCommands {
|
||||||
|
/// List accounts
|
||||||
|
List,
|
||||||
|
/// Create empty account
|
||||||
|
Create(AccountCreateArgs),
|
||||||
|
/// Update balance value
|
||||||
|
Balance(AccountBalanceArgs),
|
||||||
|
/// Approve account answer for quest
|
||||||
|
Complete(AccountCompleteArgs),
|
||||||
|
/// Delete account
|
||||||
|
Delete(AccountDeleteArgs),
|
||||||
|
/// Unlock room for account if it has enough balance
|
||||||
|
Unlock(AccountUnlockArgs),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Args)]
|
||||||
|
pub struct AccountCreateArgs {
|
||||||
|
/// Account will be created with this id
|
||||||
|
pub id: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, PartialEq, Eq, ValueEnum)]
|
||||||
|
pub enum AccountBalanceActions {
|
||||||
|
Set,
|
||||||
|
Add,
|
||||||
|
Remove,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Args)]
|
||||||
|
pub struct AccountBalanceArgs {
|
||||||
|
/// Account id
|
||||||
|
pub id: String,
|
||||||
|
/// What to do with the balance
|
||||||
|
#[arg(value_enum)]
|
||||||
|
pub action: AccountBalanceActions,
|
||||||
|
/// Amount of doing
|
||||||
|
pub value: u32,
|
||||||
|
/// If action is remove, set balance to 0 if the result is negative instead of returning error
|
||||||
|
#[arg(short,long)]
|
||||||
|
pub negative_ok: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Args)]
|
||||||
|
pub struct AccountCompleteArgs {
|
||||||
|
/// Id of the account
|
||||||
|
pub account: String,
|
||||||
|
/// Id of the quest
|
||||||
|
pub quest: u16,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Args)]
|
||||||
|
pub struct AccountDeleteArgs {
|
||||||
|
/// Id of the account to delete
|
||||||
|
pub id: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Args)]
|
||||||
|
pub struct AccountUnlockArgs {
|
||||||
|
/// Id of the account
|
||||||
|
pub account: String,
|
||||||
|
/// Id of the room to unlock
|
||||||
|
pub room: u16,
|
||||||
|
}
|
||||||
57
cli/src/cli/map.rs
Normal file
57
cli/src/cli/map.rs
Normal file
|
|
@ -0,0 +1,57 @@
|
||||||
|
use clap::{Args,Subcommand};
|
||||||
|
|
||||||
|
#[derive(Subcommand)]
|
||||||
|
pub enum MapCommands {
|
||||||
|
/// List all rooms with connections
|
||||||
|
List,
|
||||||
|
/// Add new room to map
|
||||||
|
Add(MapAddArgs),
|
||||||
|
/// Connect two rooms
|
||||||
|
Connect(MapConnectArgs),
|
||||||
|
/// Disconnect two rooms if they're connected
|
||||||
|
Disconnect(MapConnectArgs),
|
||||||
|
/// Remove all connections with the room
|
||||||
|
Delete(MapDeleteArgs),
|
||||||
|
/// Update room data
|
||||||
|
Update(MapUpdateArgs),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Args)]
|
||||||
|
pub struct MapAddArgs {
|
||||||
|
/// Name of the room
|
||||||
|
pub name: String,
|
||||||
|
/// Price of the room
|
||||||
|
pub value: u32,
|
||||||
|
/// Optional description for the room
|
||||||
|
#[arg(long,short)]
|
||||||
|
pub description: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Args)]
|
||||||
|
pub struct MapConnectArgs {
|
||||||
|
/// First room ID
|
||||||
|
pub first: u16,
|
||||||
|
/// Second room ID
|
||||||
|
pub second: u16,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Args)]
|
||||||
|
pub struct MapDeleteArgs {
|
||||||
|
/// ID of the room to delete
|
||||||
|
pub id: u16,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Args)]
|
||||||
|
pub struct MapUpdateArgs {
|
||||||
|
/// ID of the room to update
|
||||||
|
pub id: u16,
|
||||||
|
/// Room name
|
||||||
|
#[arg(short,long)]
|
||||||
|
pub name: Option<String>,
|
||||||
|
/// Room description
|
||||||
|
#[arg(short,long)]
|
||||||
|
pub description: Option<String>,
|
||||||
|
/// Room price
|
||||||
|
#[arg(short,long)]
|
||||||
|
pub value: Option<u32>,
|
||||||
|
}
|
||||||
43
cli/src/cli/mod.rs
Normal file
43
cli/src/cli/mod.rs
Normal file
|
|
@ -0,0 +1,43 @@
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
use clap::{Args,Parser,Subcommand};
|
||||||
|
|
||||||
|
pub mod account;
|
||||||
|
pub mod map;
|
||||||
|
pub mod quest;
|
||||||
|
|
||||||
|
#[derive(Parser)]
|
||||||
|
#[command(version, about, long_about = None)]
|
||||||
|
#[command(propagate_version = true)]
|
||||||
|
pub struct Cli {
|
||||||
|
/// Path to config
|
||||||
|
#[arg(short, long)]
|
||||||
|
pub config: PathBuf,
|
||||||
|
/// Object to make operation on
|
||||||
|
#[command(subcommand)]
|
||||||
|
pub command: Objects,
|
||||||
|
/// Suppress most output
|
||||||
|
#[arg(short, long)]
|
||||||
|
pub quiet: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Subcommand)]
|
||||||
|
pub enum Objects {
|
||||||
|
/// Initialize new SquadQuest in current working directory
|
||||||
|
Init(InitArgs),
|
||||||
|
/// Operations on the quests
|
||||||
|
#[command(subcommand)]
|
||||||
|
Quest(quest::QuestCommands),
|
||||||
|
/// Operations on the accounts
|
||||||
|
#[command(subcommand)]
|
||||||
|
Account(account::AccountCommands),
|
||||||
|
/// Operations on the map rooms
|
||||||
|
#[command(subcommand)]
|
||||||
|
Map(map::MapCommands),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Args)]
|
||||||
|
pub struct InitArgs {
|
||||||
|
#[arg(long,short)]
|
||||||
|
pub path: Option<PathBuf>,
|
||||||
|
}
|
||||||
131
cli/src/cli/quest.rs
Normal file
131
cli/src/cli/quest.rs
Normal file
|
|
@ -0,0 +1,131 @@
|
||||||
|
use squad_quest::quest::QuestDifficulty as LibQuestDifficulty;
|
||||||
|
use toml::value::Date;
|
||||||
|
use serde::Deserialize;
|
||||||
|
use clap::{Args,Subcommand,ValueEnum};
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
struct DateWrapper {
|
||||||
|
date: Date,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_date(arg: &str) -> Result<Date,toml::de::Error> {
|
||||||
|
let toml_str = format!("date = {arg}");
|
||||||
|
let wrapper: DateWrapper = toml::from_str(&toml_str)?;
|
||||||
|
Ok(wrapper.date)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum)]
|
||||||
|
pub enum QuestDifficulty {
|
||||||
|
/// Easy quest
|
||||||
|
Easy,
|
||||||
|
/// Normal quest
|
||||||
|
Normal,
|
||||||
|
/// Hard quest
|
||||||
|
Hard,
|
||||||
|
/// Special case of hard quests.
|
||||||
|
Secret,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<QuestDifficulty> for LibQuestDifficulty {
|
||||||
|
fn from(value: QuestDifficulty) -> Self {
|
||||||
|
match value {
|
||||||
|
QuestDifficulty::Easy => LibQuestDifficulty::Easy,
|
||||||
|
QuestDifficulty::Normal => LibQuestDifficulty::Normal,
|
||||||
|
QuestDifficulty::Hard => LibQuestDifficulty::Hard,
|
||||||
|
QuestDifficulty::Secret => LibQuestDifficulty::Secret,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Subcommand)]
|
||||||
|
pub enum QuestCommands {
|
||||||
|
/// List available quests
|
||||||
|
List(QuestListArgs),
|
||||||
|
/// Create new quest and automatically assign it id
|
||||||
|
Create(QuestCreateArgs),
|
||||||
|
/// Update existing quest
|
||||||
|
Update(QuestUpdateArgs),
|
||||||
|
/// Delete quest
|
||||||
|
Delete(QuestDeleteArgs),
|
||||||
|
/// Make certain quests public
|
||||||
|
Daily,
|
||||||
|
/// Publish quest with specified id
|
||||||
|
Publish(QuestPublishArgs),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#[derive(Args)]
|
||||||
|
pub struct QuestListArgs {
|
||||||
|
/// Only list id and name of the quest
|
||||||
|
#[arg(short, long)]
|
||||||
|
pub short: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Args)]
|
||||||
|
pub struct QuestCreateArgs {
|
||||||
|
/// Difficulty of the quest
|
||||||
|
#[arg(value_enum)]
|
||||||
|
pub difficulty: QuestDifficulty,
|
||||||
|
/// Reward for the quest
|
||||||
|
pub reward: u32,
|
||||||
|
/// Name of the quest
|
||||||
|
pub name: String,
|
||||||
|
/// Visible description of the quest
|
||||||
|
pub description: String,
|
||||||
|
/// Answer for the quest for admins
|
||||||
|
pub answer: String,
|
||||||
|
/// Create quest and make it public immediately
|
||||||
|
#[arg(short,long)]
|
||||||
|
pub public: bool,
|
||||||
|
/// Make quest available on date (format = YYYY-MM-DD, ex. 2025-12-24)
|
||||||
|
#[arg(short,long,value_parser = parse_date)]
|
||||||
|
pub available: Option<Date>,
|
||||||
|
/// Quest expiration date (format = YYYY-MM-DD, ex. 2025-12-24)
|
||||||
|
#[arg(short,long,value_parser = parse_date)]
|
||||||
|
pub deadline: Option<Date>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Args)]
|
||||||
|
pub struct QuestUpdateArgs {
|
||||||
|
/// Id of the quest to update
|
||||||
|
pub id: u16,
|
||||||
|
/// Difficulty of the quest
|
||||||
|
#[arg(value_enum,long)]
|
||||||
|
pub difficulty: Option<QuestDifficulty>,
|
||||||
|
/// Reward for the quest
|
||||||
|
#[arg(long)]
|
||||||
|
pub reward: Option<u32>,
|
||||||
|
/// Name of the quest
|
||||||
|
#[arg(long)]
|
||||||
|
pub name: Option<String>,
|
||||||
|
/// Visible description of the quest
|
||||||
|
#[arg(long)]
|
||||||
|
pub description: Option<String>,
|
||||||
|
/// Answer for the quest for admins
|
||||||
|
#[arg(long)]
|
||||||
|
pub answer: Option<String>,
|
||||||
|
/// Create quest and make it public immediately
|
||||||
|
#[arg(long)]
|
||||||
|
pub public: Option<bool>,
|
||||||
|
/// Make quest available on date (format = YYYY-MM-DD, ex. 2025-12-24)
|
||||||
|
#[arg(long,value_parser = parse_date)]
|
||||||
|
pub available: Option<Date>,
|
||||||
|
/// Quest expiration date (format = YYYY-MM-DD, ex. 2025-12-24)
|
||||||
|
#[arg(long,value_parser = parse_date)]
|
||||||
|
pub deadline: Option<Date>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Args)]
|
||||||
|
pub struct QuestDeleteArgs {
|
||||||
|
/// Id of the quest to delete
|
||||||
|
pub id: u16,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Args)]
|
||||||
|
pub struct QuestPublishArgs {
|
||||||
|
/// Id of the quest to publish
|
||||||
|
pub id: u16,
|
||||||
|
/// Make it non-public instead
|
||||||
|
#[arg(long,short)]
|
||||||
|
pub reverse: bool,
|
||||||
|
}
|
||||||
1
cli/src/lib.rs
Normal file
1
cli/src/lib.rs
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
pub mod cli;
|
||||||
450
cli/src/main.rs
Normal file
450
cli/src/main.rs
Normal file
|
|
@ -0,0 +1,450 @@
|
||||||
|
use std::{fs::DirBuilder, path::{Path, PathBuf}};
|
||||||
|
|
||||||
|
use clap::Parser;
|
||||||
|
use squad_quest_cli::cli::{Cli,Objects,account::*,map::*,quest::*};
|
||||||
|
use squad_quest::{SquadObject, account::Account, config::Config, error::Error, map::{Map, Room}, quest::Quest};
|
||||||
|
use toml::value::Date;
|
||||||
|
use chrono::{Datelike, NaiveDate, Utc};
|
||||||
|
|
||||||
|
fn print_quest_short(quest: &Quest) {
|
||||||
|
println!("Quest #{}: {}", quest.id, quest.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn print_quest_long(quest: &Quest) {
|
||||||
|
print_quest_short(quest);
|
||||||
|
println!("Difficulty: {:?}", quest.difficulty);
|
||||||
|
println!("Description:\n{}", quest.description);
|
||||||
|
println!("Answer:\n{}", quest.answer);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn do_and_log(result: Result<(),Error>, log: bool, ok_text: String) {
|
||||||
|
match result {
|
||||||
|
Ok(_) if log => println!("{ok_text}"),
|
||||||
|
Err(error) if log => eprintln!("Error: {error}"),
|
||||||
|
_ => {},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn load_config_silent(quiet: bool, path: PathBuf) -> Config {
|
||||||
|
match quiet {
|
||||||
|
false => Config::load(path.clone()),
|
||||||
|
true => {
|
||||||
|
match Config::try_load(path.clone()) {
|
||||||
|
Ok(mut config) => {
|
||||||
|
config.verbose = false;
|
||||||
|
config
|
||||||
|
},
|
||||||
|
Err(_) => {
|
||||||
|
let path = path.clone().parent().unwrap_or(&Path::new(".")).to_owned();
|
||||||
|
Config {
|
||||||
|
verbose: false,
|
||||||
|
path,
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
let cli = Cli::parse();
|
||||||
|
|
||||||
|
let config = load_config_silent(cli.quiet, cli.config.clone());
|
||||||
|
let map_save = |map: Map, map_path: PathBuf| { map.save(map_path.parent().unwrap_or(Path::new("")).to_owned()) };
|
||||||
|
|
||||||
|
match &cli.command {
|
||||||
|
Objects::Init(args) => {
|
||||||
|
let path = match args.path.clone() {
|
||||||
|
Some(path) => path,
|
||||||
|
None => PathBuf::new(),
|
||||||
|
};
|
||||||
|
|
||||||
|
match DirBuilder::new().recursive(true).create(path.clone()) {
|
||||||
|
Ok(_) if !cli.quiet => println!("Created directory {:?}", path),
|
||||||
|
Err(error) => {
|
||||||
|
if !cli.quiet { eprintln!("Error: {error}"); }
|
||||||
|
return;
|
||||||
|
},
|
||||||
|
_ => {},
|
||||||
|
}
|
||||||
|
|
||||||
|
let config = Config {
|
||||||
|
path: path.clone(),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
do_and_log(config.save(path.clone()), !cli.quiet, format!("Created file {:?}/config.toml", path));
|
||||||
|
let mut config_path = path.clone();
|
||||||
|
config_path.push("config.toml");
|
||||||
|
let mut config = load_config_silent(true, config_path);
|
||||||
|
config.verbose = Config::default().verbose;
|
||||||
|
|
||||||
|
let map = Map::default();
|
||||||
|
let map_path = config.full_map_path();
|
||||||
|
|
||||||
|
do_and_log(map_save(map, map_path.clone()), !cli.quiet, format!("Created file {:?}/map.toml", map_path));
|
||||||
|
|
||||||
|
let quests_path = config.full_quests_path();
|
||||||
|
let accounts_path = config.full_accounts_path();
|
||||||
|
|
||||||
|
for path in [quests_path, accounts_path] {
|
||||||
|
match DirBuilder::new().recursive(true).create(path.clone()) {
|
||||||
|
Ok(_) if !cli.quiet => println!("Created directory {:?}", path),
|
||||||
|
Err(error) => {
|
||||||
|
if !cli.quiet { eprintln!("Error: {error}"); }
|
||||||
|
return;
|
||||||
|
},
|
||||||
|
_ => {},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Objects::Quest(commands) => {
|
||||||
|
let mut quests = config.load_quests();
|
||||||
|
let mut path = config.full_quests_path();
|
||||||
|
|
||||||
|
match commands {
|
||||||
|
QuestCommands::List(args) => {
|
||||||
|
for quest in quests {
|
||||||
|
if args.short {
|
||||||
|
print_quest_short(&quest);
|
||||||
|
} else {
|
||||||
|
print_quest_long(&quest);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
QuestCommands::Create(args) => {
|
||||||
|
quests.sort_by(|a,b| a.id.cmp(&b.id));
|
||||||
|
let next_id = match quests.last() {
|
||||||
|
Some(quest) => quest.id + 1u16,
|
||||||
|
None => 0u16
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut check_path = path.clone();
|
||||||
|
check_path.push(format!("{next_id}.toml"));
|
||||||
|
match std::fs::exists(&check_path) {
|
||||||
|
Ok(exists) => {
|
||||||
|
if exists {
|
||||||
|
if !cli.quiet { eprintln!("Error: {:?} is not empty.", path); }
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Err(error) => {
|
||||||
|
if !cli.quiet {
|
||||||
|
eprintln!("Error: {error}");
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let quest = Quest {
|
||||||
|
id: next_id,
|
||||||
|
difficulty: args.difficulty.into(),
|
||||||
|
reward: args.reward,
|
||||||
|
name: args.name.clone(),
|
||||||
|
description: args.description.clone(),
|
||||||
|
answer: args.answer.clone(),
|
||||||
|
public: args.public,
|
||||||
|
available_on: args.available.clone(),
|
||||||
|
deadline: args.deadline.clone()
|
||||||
|
};
|
||||||
|
|
||||||
|
do_and_log(quest.save(path), !cli.quiet, format!("Created quest #{}.", quest.id));
|
||||||
|
},
|
||||||
|
QuestCommands::Update(args) => {
|
||||||
|
let Some(quest) = quests.iter().find(|q| q.id == args.id) else {
|
||||||
|
if !cli.quiet { eprintln!("Error: Quest #{} not found.", args.id); }
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let quest = Quest {
|
||||||
|
id: args.id,
|
||||||
|
difficulty: match args.difficulty {
|
||||||
|
Some(diff) => diff.into(),
|
||||||
|
None => quest.difficulty
|
||||||
|
},
|
||||||
|
reward: args.reward.unwrap_or(quest.reward),
|
||||||
|
name: args.name.clone().unwrap_or(quest.name.clone()),
|
||||||
|
description: args.description.clone().unwrap_or(quest.description.clone()),
|
||||||
|
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())
|
||||||
|
};
|
||||||
|
|
||||||
|
do_and_log(quest.save(path), !cli.quiet, format!("Updated quest #{}.", quest.id));
|
||||||
|
},
|
||||||
|
QuestCommands::Delete(args) => {
|
||||||
|
path.push(format!("{}.toml", args.id));
|
||||||
|
match Quest::delete(path) {
|
||||||
|
Ok(_) => {
|
||||||
|
if !cli.quiet { println!("Deleted quest #{}.", args.id); }
|
||||||
|
|
||||||
|
let mut accounts = config.load_accounts();
|
||||||
|
let accounts_path = config.full_accounts_path();
|
||||||
|
for account in accounts.iter_mut() {
|
||||||
|
if let Some(index) = account.quests_completed.iter().position(|qid| *qid == args.id) {
|
||||||
|
account.quests_completed.remove(index);
|
||||||
|
do_and_log(account.save(accounts_path.clone()), !cli.quiet, format!("Removed quest #{} from account \"{}\" completed quests", args.id, account.id));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Err(error) if !cli.quiet => {
|
||||||
|
eprintln!("Error: {error}");
|
||||||
|
},
|
||||||
|
_ => {},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
QuestCommands::Daily => {
|
||||||
|
let today: NaiveDate = Utc::now().date_naive();
|
||||||
|
let toml_today = Date {
|
||||||
|
year: today.year() as u16,
|
||||||
|
month: today.month() as u8,
|
||||||
|
day: today.day() as u8
|
||||||
|
};
|
||||||
|
|
||||||
|
for quest in quests.iter_mut().filter(|q| !q.public && q.available_on.is_some_and(|date| date.le(&toml_today))) {
|
||||||
|
quest.public = true;
|
||||||
|
do_and_log(quest.save(path.clone()), !cli.quiet, format!("Published quest #{}.", quest.id));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
QuestCommands::Publish(args) => {
|
||||||
|
let quest = quests.iter_mut().find(|q| q.id == args.id);
|
||||||
|
|
||||||
|
match quest {
|
||||||
|
Some(quest) => {
|
||||||
|
let not_str = if args.reverse {" not "} else {" "};
|
||||||
|
|
||||||
|
if quest.public != args.reverse {
|
||||||
|
if !cli.quiet { eprintln!("Error: quest #{} is already{}public.", quest.id, not_str); }
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
quest.public = !args.reverse;
|
||||||
|
do_and_log(quest.save(path), !cli.quiet, format!("Published quest #{}.", quest.id));
|
||||||
|
},
|
||||||
|
None if !cli.quiet => eprintln!("Error: quest #{} not found.", args.id),
|
||||||
|
_ => {},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Objects::Account(commands) => {
|
||||||
|
let mut accounts = config.load_accounts();
|
||||||
|
let mut path = config.full_accounts_path();
|
||||||
|
|
||||||
|
match commands {
|
||||||
|
AccountCommands::List => {
|
||||||
|
for account in accounts {
|
||||||
|
println!("\"{}\": Balance {}", account.id, account.balance);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
AccountCommands::Create(args) => {
|
||||||
|
let account = Account {
|
||||||
|
id: args.id.clone(),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(_) = accounts.iter().find(|a| a.id == account.id) {
|
||||||
|
if !cli.quiet { eprintln!("Error: account \"{}\" exists.", account.id); }
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
do_and_log(account.save(path), !cli.quiet, format!("Created account \"{}\".", account.id));
|
||||||
|
},
|
||||||
|
AccountCommands::Balance(args) => {
|
||||||
|
let Some(account) = accounts.iter_mut().find(|a| a.id == args.id) else {
|
||||||
|
if !cli.quiet { eprintln!("Error: account \"{}\" not found.", args.id); }
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
match args.action {
|
||||||
|
AccountBalanceActions::Set => {
|
||||||
|
account.balance = args.value;
|
||||||
|
},
|
||||||
|
AccountBalanceActions::Add => {
|
||||||
|
account.balance += args.value;
|
||||||
|
},
|
||||||
|
AccountBalanceActions::Remove => {
|
||||||
|
if args.value > account.balance {
|
||||||
|
if args.negative_ok {
|
||||||
|
account.balance = 0u32;
|
||||||
|
} else {
|
||||||
|
if !cli.quiet { eprintln!("Error: account \"{}\" balance is less than {}.", account.id, args.value); }
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
account.balance -= args.value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
do_and_log(account.save(path), !cli.quiet, format!("Updated balance of account \"{}\".", account.id));
|
||||||
|
},
|
||||||
|
AccountCommands::Complete(args) => {
|
||||||
|
let Some(account) = accounts.iter_mut().find(|a| a.id == args.account) else {
|
||||||
|
if !cli.quiet { eprintln!("Error: account \"{}\" not found.", args.account); }
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
let quests = config.load_quests();
|
||||||
|
|
||||||
|
let quest = match quests.iter().find(|q| q.id == args.quest) {
|
||||||
|
Some(quest) => quest,
|
||||||
|
None => {
|
||||||
|
if !cli.quiet { eprintln!("Error: quest #{} not found.", args.quest); }
|
||||||
|
return;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
match quest.complete_for_account(account) {
|
||||||
|
Err(error) if !cli.quiet => println!("Error: {error}"),
|
||||||
|
Ok(_) => do_and_log(account.save(path), !cli.quiet, format!("Completed quest #{} on account \"{}\".", args.quest, account.id)),
|
||||||
|
_ => {},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
AccountCommands::Delete(args) => {
|
||||||
|
path.push(format!("{}.toml", args.id));
|
||||||
|
do_and_log(Account::delete(path), !cli.quiet, format!("Deleted account \"{}\".", args.id))
|
||||||
|
},
|
||||||
|
AccountCommands::Unlock(args) => {
|
||||||
|
let Some(account) = accounts.iter_mut().find(|a| a.id == args.account) else {
|
||||||
|
if !cli.quiet { eprintln!("Error: account \"{}\" not found.", args.account) };
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
let map = match Map::load(config.full_map_path()) {
|
||||||
|
Ok(map) => map,
|
||||||
|
Err(error) => {
|
||||||
|
if !cli.quiet { eprintln!("Error: {error}"); }
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Err(error) = map.unlock_room_for_account(args.room, account) {
|
||||||
|
eprintln!("Error: {error}");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
do_and_log(account.save(path), !cli.quiet, format!("Unlocked room #{} for account \"{}\"", args.room, args.account));
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Objects::Map(commands) => {
|
||||||
|
let map_path = config.full_map_path();
|
||||||
|
let mut map = match Map::load(map_path.clone()) {
|
||||||
|
Ok(map) => map,
|
||||||
|
Err(error) => {
|
||||||
|
if !cli.quiet { eprintln!("Error: {error}"); }
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
match commands {
|
||||||
|
MapCommands::List => {
|
||||||
|
for room in map.room {
|
||||||
|
println!("Room #{}: {}; Connections: {:?}", room.id, room.name, room.children);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
MapCommands::Add(args) => {
|
||||||
|
map.room.sort_by(|a,b| a.id.cmp(&b.id));
|
||||||
|
let last_id = match map.room.last() {
|
||||||
|
Some(r) => r.id + 1u16,
|
||||||
|
None => 0u16
|
||||||
|
};
|
||||||
|
let room = Room {
|
||||||
|
id: last_id,
|
||||||
|
name: args.name.clone(),
|
||||||
|
value: args.value,
|
||||||
|
description: args.description.clone(),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
let r_id = room.id;
|
||||||
|
map.room.push(room);
|
||||||
|
do_and_log(map_save(map, map_path), !cli.quiet, format!("Created room #{r_id}."))
|
||||||
|
},
|
||||||
|
MapCommands::Delete(args) => {
|
||||||
|
let Some(room) = map.room.iter().find(|r| r.id == args.id) else {
|
||||||
|
if !cli.quiet { eprintln!("Error: room #{} not found.", args.id); }
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
let r_id = room.id;
|
||||||
|
let index = map.room.iter().position(|r| r.eq(room)).unwrap();
|
||||||
|
map.room.remove(index);
|
||||||
|
|
||||||
|
for room in map.room.iter_mut().filter(|r| r.children.contains(&r_id)) {
|
||||||
|
let idx = room.children.iter()
|
||||||
|
.position(|id| *id == r_id)
|
||||||
|
.unwrap();
|
||||||
|
room.children.remove(idx);
|
||||||
|
}
|
||||||
|
|
||||||
|
match map_save(map, map_path) {
|
||||||
|
Ok(_) => {
|
||||||
|
if !cli.quiet { println!("Deleted room #{r_id}."); }
|
||||||
|
|
||||||
|
let mut accounts = config.load_accounts();
|
||||||
|
let accounts_path = config.full_accounts_path();
|
||||||
|
|
||||||
|
for account in accounts.iter_mut() {
|
||||||
|
if let Some(index) = account.rooms_unlocked.iter().position(|rid| *rid == r_id) {
|
||||||
|
account.rooms_unlocked.remove(index);
|
||||||
|
do_and_log(account.save(accounts_path.clone()), !cli.quiet, format!("Removed room #{r_id} from account \"{}\" unlocked rooms.", account.id));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Err(error) if !cli.quiet => {
|
||||||
|
eprintln!("Error: {error}");
|
||||||
|
},
|
||||||
|
_ => {},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
MapCommands::Update(args) => {
|
||||||
|
let Some(room) = map.room.iter_mut().find(|r| r.id == args.id) else {
|
||||||
|
if !cli.quiet { eprintln!("Error: room #{} not found", args.id); }
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(name) = &args.name {
|
||||||
|
room.name = name.to_string();
|
||||||
|
}
|
||||||
|
|
||||||
|
if args.description.is_some() {
|
||||||
|
room.description = args.description.clone();
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(value) = args.value {
|
||||||
|
room.value = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
do_and_log(map_save(map, map_path), !cli.quiet, format!("Updated room #{}.", args.id))
|
||||||
|
},
|
||||||
|
MapCommands::Connect(args) | MapCommands::Disconnect(args) => {
|
||||||
|
let connect = match commands {
|
||||||
|
MapCommands::Connect(_) => true,
|
||||||
|
_ => false,
|
||||||
|
};
|
||||||
|
|
||||||
|
// We iterate twice to make references first->second and second->first
|
||||||
|
for (first, second) in [(args.first, args.second),(args.second, args.first)] {
|
||||||
|
let Some(room) = map.room.iter_mut().find(|r| r.id == first) else {
|
||||||
|
if !cli.quiet { eprintln!("Error: room #{} not found.", first); }
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
match room.children.iter().position(|id| *id == second) {
|
||||||
|
Some(_) if connect && !cli.quiet => println!("Room #{} already has reference to #{}.", first, second),
|
||||||
|
None if connect => room.children.push(second),
|
||||||
|
Some(id) if !connect => {room.children.remove(id as usize);},
|
||||||
|
None if !connect && !cli.quiet => println!("Room #{} has no reference to #{}.", first, second),
|
||||||
|
_ => {},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let connected = if connect { "Connected" } else { "Disconnected" };
|
||||||
|
do_and_log(map_save(map, map_path), !cli.quiet, format!("{connected} rooms #{} <-> #{}.", args.first, args.second));
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,24 +1,97 @@
|
||||||
//! User accounts
|
//! User accounts
|
||||||
|
|
||||||
|
use std::{fs, io::Write, path::PathBuf};
|
||||||
|
|
||||||
use serde::{ Serialize, Deserialize };
|
use serde::{ Serialize, Deserialize };
|
||||||
|
|
||||||
|
use crate::{SquadObject, error::Error};
|
||||||
|
|
||||||
fn default_id() -> String {
|
fn default_id() -> String {
|
||||||
"none".to_string()
|
"none".to_string()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// User account struct, which can be (de-)serialized from/into TOML
|
/// User account struct, which can be (de-)serialized from/into TOML
|
||||||
#[derive(Serialize, Deserialize)]
|
#[derive(Serialize, Deserialize, PartialEq, Debug)]
|
||||||
|
#[serde(default)]
|
||||||
pub struct Account {
|
pub struct Account {
|
||||||
|
|
||||||
/// User identifier, specific to used service
|
/// User identifier, specific to used service
|
||||||
#[serde(default = "default_id")]
|
|
||||||
pub id: String,
|
pub id: String,
|
||||||
|
|
||||||
/// User balance
|
/// User balance
|
||||||
#[serde(default)]
|
|
||||||
pub balance: u32,
|
pub balance: u32,
|
||||||
|
|
||||||
/// Id of room node where user is located
|
/// Id of room node where user is located
|
||||||
#[serde(default)]
|
pub location: u16,
|
||||||
pub location: u16
|
|
||||||
|
/// Vec of quests completed by this user
|
||||||
|
pub quests_completed: Vec<u16>,
|
||||||
|
|
||||||
|
/// Vec of rooms unlocked by this user
|
||||||
|
pub rooms_unlocked: Vec<u16>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Account {
|
||||||
|
fn default() -> Self {
|
||||||
|
Account {
|
||||||
|
id: default_id(),
|
||||||
|
balance: u32::default(),
|
||||||
|
location: u16::default(),
|
||||||
|
quests_completed: Vec::new(),
|
||||||
|
rooms_unlocked: Vec::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SquadObject for Account {
|
||||||
|
fn load(path: PathBuf) -> Result<Self, Error> {
|
||||||
|
match std::fs::read_to_string(path) {
|
||||||
|
Ok(string) => {
|
||||||
|
match toml::from_str::<Self>(&string) {
|
||||||
|
Ok(object) => Ok(object),
|
||||||
|
Err(error) => Err(Error::TomlDeserializeError(error))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Err(error) => Err(Error::IoError(error))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn delete(path: PathBuf) -> Result<(), Error> {
|
||||||
|
match Self::load(path.clone()) {
|
||||||
|
Ok(_) => {
|
||||||
|
if let Err(error) = fs::remove_file(path) {
|
||||||
|
return Err(Error::IoError(error));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
},
|
||||||
|
Err(error) => Err(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn save(&self, path: PathBuf) -> Result<(), Error> {
|
||||||
|
let filename = format!("{}.toml", self.id);
|
||||||
|
let mut full_path = path;
|
||||||
|
full_path.push(filename);
|
||||||
|
|
||||||
|
let str = match toml::to_string_pretty(&self) {
|
||||||
|
Ok(string) => string,
|
||||||
|
Err(error) => {
|
||||||
|
return Err(Error::TomlSerializeError(error));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut file = match fs::File::create(full_path) {
|
||||||
|
Ok(f) => f,
|
||||||
|
Err(error) => {
|
||||||
|
return Err(Error::IoError(error));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Err(error) = file.write_all(str.as_bytes()) {
|
||||||
|
return Err(Error::IoError(error));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
210
src/bin/cli.rs
210
src/bin/cli.rs
|
|
@ -1,210 +0,0 @@
|
||||||
use std::path::PathBuf;
|
|
||||||
|
|
||||||
use clap::{Parser,Subcommand,Args,ValueEnum};
|
|
||||||
use squad_quest::{config::Config,quest::{Quest,QuestDifficulty as LibQuestDifficulty}};
|
|
||||||
|
|
||||||
#[derive(Parser)]
|
|
||||||
#[command(version, about, long_about = None)]
|
|
||||||
#[command(propagate_version = true)]
|
|
||||||
struct Cli {
|
|
||||||
/// Path to config
|
|
||||||
#[arg(short, long)]
|
|
||||||
config: PathBuf,
|
|
||||||
/// Object to make operation on
|
|
||||||
#[command(subcommand)]
|
|
||||||
command: Objects,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Subcommand)]
|
|
||||||
enum Objects {
|
|
||||||
/// Operations on the quests
|
|
||||||
#[command(subcommand)]
|
|
||||||
Quest(QuestCommands)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum)]
|
|
||||||
enum QuestDifficulty {
|
|
||||||
/// Easy quest
|
|
||||||
Easy,
|
|
||||||
/// Normal quest
|
|
||||||
Normal,
|
|
||||||
/// Hard quest
|
|
||||||
Hard,
|
|
||||||
/// Special case of hard quests.
|
|
||||||
Secret
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<QuestDifficulty> for LibQuestDifficulty {
|
|
||||||
fn from(value: QuestDifficulty) -> Self {
|
|
||||||
match value {
|
|
||||||
QuestDifficulty::Easy => LibQuestDifficulty::Easy,
|
|
||||||
QuestDifficulty::Normal => LibQuestDifficulty::Normal,
|
|
||||||
QuestDifficulty::Hard => LibQuestDifficulty::Hard,
|
|
||||||
QuestDifficulty::Secret => LibQuestDifficulty::Secret,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Subcommand)]
|
|
||||||
enum QuestCommands {
|
|
||||||
/// List available quests
|
|
||||||
List(QuestListArgs),
|
|
||||||
/// Create new quest and automatically assign it id
|
|
||||||
Create(QuestCreateArgs),
|
|
||||||
/// Update existing quest
|
|
||||||
Update(QuestUpdateArgs),
|
|
||||||
/// Delete quest
|
|
||||||
Delete(QuestDeleteArgs),
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Args)]
|
|
||||||
struct QuestListArgs {
|
|
||||||
/// Only list id and name of the quest
|
|
||||||
#[arg(short, long)]
|
|
||||||
short: bool
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Args)]
|
|
||||||
struct QuestCreateArgs {
|
|
||||||
/// Difficulty of the quest
|
|
||||||
#[arg(value_enum)]
|
|
||||||
difficulty: QuestDifficulty,
|
|
||||||
/// Reward for the quest
|
|
||||||
reward: u32,
|
|
||||||
/// Name of the quest
|
|
||||||
name: String,
|
|
||||||
/// Visible description of the quest
|
|
||||||
description: String,
|
|
||||||
/// Answer for the quest for admins
|
|
||||||
answer: String
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Args)]
|
|
||||||
struct QuestUpdateArgs {
|
|
||||||
/// Id of the quest to update
|
|
||||||
id: u16,
|
|
||||||
/// Difficulty of the quest
|
|
||||||
#[arg(value_enum,long)]
|
|
||||||
difficulty: Option<QuestDifficulty>,
|
|
||||||
/// Reward for the quest
|
|
||||||
#[arg(long)]
|
|
||||||
reward: Option<u32>,
|
|
||||||
/// Name of the quest
|
|
||||||
#[arg(long)]
|
|
||||||
name: Option<String>,
|
|
||||||
/// Visible description of the quest
|
|
||||||
#[arg(long)]
|
|
||||||
description: Option<String>,
|
|
||||||
/// Answer for the quest for admins
|
|
||||||
#[arg(long)]
|
|
||||||
answer: Option<String>
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Args)]
|
|
||||||
struct QuestDeleteArgs {
|
|
||||||
/// Id of the quest to delete
|
|
||||||
id: u16
|
|
||||||
}
|
|
||||||
|
|
||||||
fn print_quest_short(quest: &Quest) {
|
|
||||||
println!("Quest #{}: {}", quest.id, quest.name);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn print_quest_long(quest: &Quest) {
|
|
||||||
print_quest_short(quest);
|
|
||||||
println!("Difficulty: {:?}", quest.difficulty);
|
|
||||||
println!("Description:\n{}", quest.description);
|
|
||||||
println!("Answer:\n{}", quest.answer);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn main() {
|
|
||||||
let cli = Cli::parse();
|
|
||||||
|
|
||||||
let config = Config::load(cli.config.clone());
|
|
||||||
|
|
||||||
match &cli.command {
|
|
||||||
Objects::Quest(commands) => {
|
|
||||||
match commands {
|
|
||||||
QuestCommands::List(args) => {
|
|
||||||
let quests = config.load_quests();
|
|
||||||
for quest in quests {
|
|
||||||
if args.short {
|
|
||||||
print_quest_short(&quest);
|
|
||||||
} else {
|
|
||||||
print_quest_long(&quest);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
QuestCommands::Create(args) => {
|
|
||||||
let mut quests = config.load_quests();
|
|
||||||
quests.sort_by(|a,b| a.id.cmp(&b.id));
|
|
||||||
let next_id = match quests.last() {
|
|
||||||
Some(quest) if quest.id == u16::MAX => {
|
|
||||||
panic!("Error: quest list contains quest with u16::MAX id.");
|
|
||||||
}
|
|
||||||
Some(quest) => quest.id + 1u16,
|
|
||||||
None => 0u16
|
|
||||||
};
|
|
||||||
|
|
||||||
let path = config.full_quests_path();
|
|
||||||
let mut quest_path = path.clone();
|
|
||||||
quest_path.push(format!("{next_id}.toml"));
|
|
||||||
match std::fs::exists(&quest_path) {
|
|
||||||
Ok(exists) => {
|
|
||||||
if exists {
|
|
||||||
panic!("Error: {:?} is not empty.", quest_path);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
Err(error) => {
|
|
||||||
panic!("Error while retrieving {:?}: {}.", quest_path, error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let quest = Quest {
|
|
||||||
id: next_id,
|
|
||||||
difficulty: args.difficulty.into(),
|
|
||||||
reward: args.reward,
|
|
||||||
name: args.name.clone(),
|
|
||||||
description: args.description.clone(),
|
|
||||||
answer: args.answer.clone(),
|
|
||||||
};
|
|
||||||
if let Err(error) = quest.save(path) {
|
|
||||||
eprintln!("Error while saving quest: {error}.");
|
|
||||||
} else {
|
|
||||||
println!("Successfully saved quest #{}.", quest.id);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
QuestCommands::Update(args) => {
|
|
||||||
let quests = config.load_quests();
|
|
||||||
let Some(quest) = quests.iter().find(|q| q.id == args.id) else {
|
|
||||||
panic!("Error: Quest #{} not found.", args.id);
|
|
||||||
};
|
|
||||||
let quest = Quest {
|
|
||||||
id: args.id,
|
|
||||||
difficulty: match args.difficulty {
|
|
||||||
Some(diff) => diff.into(),
|
|
||||||
None => quest.difficulty
|
|
||||||
},
|
|
||||||
reward: args.reward.unwrap_or(quest.reward),
|
|
||||||
name: args.name.clone().unwrap_or(quest.name.clone()),
|
|
||||||
description: args.description.clone().unwrap_or(quest.description.clone()),
|
|
||||||
answer: args.answer.clone().unwrap_or(quest.answer.clone())
|
|
||||||
};
|
|
||||||
let path = config.full_quests_path();
|
|
||||||
match quest.save(path) {
|
|
||||||
Ok(_) => println!("Updated quest #{}", quest.id),
|
|
||||||
Err(error) => eprintln!("Error while updating quest: {error}")
|
|
||||||
}
|
|
||||||
},
|
|
||||||
QuestCommands::Delete(args) => {
|
|
||||||
let mut path = config.full_quests_path();
|
|
||||||
path.push(format!("{}.toml", args.id));
|
|
||||||
match Quest::delete(path) {
|
|
||||||
Ok(_) => println!("Successfully deleted quest #{}", args.id),
|
|
||||||
Err(error) => eprintln!("Error deleting quest #{}: {}", args.id, error),
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,17 +1,17 @@
|
||||||
//! Configuration file that handles (de-)serializing other components
|
//! Configuration file that handles (de-)serializing other components
|
||||||
|
|
||||||
use std::{fs::{self, DirEntry},path::{Path, PathBuf}};
|
use std::{fs::{self, DirEntry}, io::Write, path::{Path, PathBuf}};
|
||||||
use serde::Deserialize;
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
use crate::quest::{Quest,error::QuestError};
|
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(Deserialize)]
|
#[derive(Serialize, Deserialize)]
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub struct Config {
|
pub struct Config {
|
||||||
/// Path to config directory
|
/// Path to config directory
|
||||||
#[serde(skip)]
|
#[serde(skip)]
|
||||||
path: PathBuf,
|
pub path: PathBuf,
|
||||||
|
|
||||||
/// Path to serialized [quests][`crate::quest::Quest`] folder
|
/// Path to serialized [quests][`crate::quest::Quest`] folder
|
||||||
pub quests_path: PathBuf,
|
pub quests_path: PathBuf,
|
||||||
|
|
@ -20,7 +20,10 @@ pub struct Config {
|
||||||
pub accounts_path: PathBuf,
|
pub accounts_path: PathBuf,
|
||||||
|
|
||||||
/// Path to serialized [map][`crate::map::Map`] file
|
/// Path to serialized [map][`crate::map::Map`] file
|
||||||
pub map: PathBuf
|
pub map: PathBuf,
|
||||||
|
|
||||||
|
/// If true, print to std{out/err}
|
||||||
|
pub verbose: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for Config {
|
impl Default for Config {
|
||||||
|
|
@ -29,30 +32,51 @@ impl Default for Config {
|
||||||
path: ".".into(),
|
path: ".".into(),
|
||||||
quests_path: "quests".into(),
|
quests_path: "quests".into(),
|
||||||
accounts_path: "accounts".into(),
|
accounts_path: "accounts".into(),
|
||||||
map: "map.toml".into()
|
map: "map.toml".into(),
|
||||||
|
verbose: true,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn handle_quest_entry(quest_entry: DirEntry) -> Result<Quest, QuestError>{
|
fn handle_quest_entry(quest_entry: DirEntry) -> Result<Quest, Error>{
|
||||||
let filetype = quest_entry.file_type();
|
let filetype = quest_entry.file_type();
|
||||||
if let Err(error) = filetype {
|
if let Err(error) = filetype {
|
||||||
return Err(QuestError::IoError(error));
|
return Err(Error::IoError(error));
|
||||||
}
|
}
|
||||||
|
|
||||||
let path = quest_entry.path();
|
let path = quest_entry.path();
|
||||||
|
|
||||||
let filetype = filetype.unwrap();
|
let filetype = filetype.unwrap();
|
||||||
if !filetype.is_file() {
|
if !filetype.is_file() {
|
||||||
return Err(QuestError::IsNotAFile(path));
|
return Err(Error::IsNotAFile(path));
|
||||||
}
|
}
|
||||||
|
|
||||||
Quest::load(path)
|
Quest::load(path)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn handle_account_entry(account_entry: DirEntry) -> Result<Account, Error>{
|
||||||
|
let filetype = account_entry.file_type();
|
||||||
|
if let Err(error) = filetype {
|
||||||
|
return Err(Error::IoError(error));
|
||||||
|
}
|
||||||
|
|
||||||
|
let path = account_entry.path();
|
||||||
|
|
||||||
|
let filetype = filetype.unwrap();
|
||||||
|
if !filetype.is_file() {
|
||||||
|
return Err(Error::IsNotAFile(path));
|
||||||
|
}
|
||||||
|
|
||||||
|
Account::load(path)
|
||||||
|
}
|
||||||
|
|
||||||
impl Config {
|
impl Config {
|
||||||
/// Deserialize config from TOML
|
/// Deserialize config from TOML.
|
||||||
/// Logs all errors and returns default config if that happens
|
///
|
||||||
|
/// This function wraps [try_load][Config::try_load].
|
||||||
|
///
|
||||||
|
/// Logs all errors if `config.verbose == true`.
|
||||||
|
/// Returns default config on error.
|
||||||
///
|
///
|
||||||
/// # Examples
|
/// # Examples
|
||||||
/// ```rust
|
/// ```rust
|
||||||
|
|
@ -65,25 +89,98 @@ impl Config {
|
||||||
let dir = path.parent()
|
let dir = path.parent()
|
||||||
.unwrap_or(Path::new("."))
|
.unwrap_or(Path::new("."))
|
||||||
.to_owned();
|
.to_owned();
|
||||||
|
|
||||||
|
match Self::try_load(path) {
|
||||||
|
Ok(conf) => {
|
||||||
|
if conf.verbose {
|
||||||
|
println!("Successfully loaded config");
|
||||||
|
}
|
||||||
|
conf
|
||||||
|
},
|
||||||
|
Err(error) => {
|
||||||
|
let conf = Config {
|
||||||
|
path: dir,
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
if conf.verbose {
|
||||||
|
println!("Error while loading config: {error}");
|
||||||
|
}
|
||||||
|
|
||||||
|
conf
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Serialize config into TOML.
|
||||||
|
/// Config will be saved as `path/config.toml`
|
||||||
|
///
|
||||||
|
/// # Examples
|
||||||
|
/// ```rust
|
||||||
|
/// use squad_quest::config::Config;
|
||||||
|
///
|
||||||
|
/// let path = "cfg".into();
|
||||||
|
///
|
||||||
|
/// let config = Config::default();
|
||||||
|
///
|
||||||
|
/// if let Err(error) = config.save(path) {
|
||||||
|
/// // handle error
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
pub fn save(&self, path: PathBuf) -> Result<(), Error> {
|
||||||
|
let mut path = path;
|
||||||
|
path.push("config.toml");
|
||||||
|
|
||||||
|
let str = match toml::to_string_pretty(&self) {
|
||||||
|
Ok(string) => string,
|
||||||
|
Err(error) => return Err(Error::TomlSerializeError(error)),
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut file = match fs::File::create(path) {
|
||||||
|
Ok(f) => f,
|
||||||
|
Err(error) => return Err(Error::IoError(error)),
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Err(error) = file.write_all(str.as_bytes()) {
|
||||||
|
return Err(Error::IoError(error));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Deserialize config from TOML
|
||||||
|
///
|
||||||
|
/// # Examples
|
||||||
|
/// ```rust
|
||||||
|
/// use squad_quest::{config::Config,error::Error};
|
||||||
|
/// # fn main() {
|
||||||
|
/// # let _ = wrapper();
|
||||||
|
/// # }
|
||||||
|
/// # fn wrapper() -> Result<(), Error> {
|
||||||
|
/// let path = "cfg/config.toml".into();
|
||||||
|
/// let config = Config::try_load(path)?;
|
||||||
|
/// # Ok(())
|
||||||
|
/// # }
|
||||||
|
/// ```
|
||||||
|
pub fn try_load(path: PathBuf) -> Result<Self, Error> {
|
||||||
|
let dir = path.parent()
|
||||||
|
.unwrap_or(Path::new("."))
|
||||||
|
.to_owned();
|
||||||
|
|
||||||
match fs::read_to_string(path) {
|
match fs::read_to_string(path) {
|
||||||
Ok(string) => {
|
Ok(string) => {
|
||||||
match toml::from_str::<Config>(&string) {
|
match toml::from_str::<Config>(&string) {
|
||||||
Ok(mut conf) => {
|
Ok(mut conf) => {
|
||||||
println!("Successfully loaded config");
|
|
||||||
conf.path = dir;
|
conf.path = dir;
|
||||||
conf
|
Ok(conf)
|
||||||
},
|
},
|
||||||
Err(error) => {
|
Err(error) => {
|
||||||
eprintln!("Error on parsing config: {error}");
|
Err(Error::TomlDeserializeError(error))
|
||||||
let mut cfg = Config::default();
|
|
||||||
cfg.path = dir;
|
|
||||||
cfg
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
Err(error) => {
|
Err(error) => {
|
||||||
eprintln!("Error on reading config path: {error}");
|
Err(Error::IoError(error))
|
||||||
Config::default()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -134,24 +231,126 @@ impl Config {
|
||||||
Ok(quest_entry) => {
|
Ok(quest_entry) => {
|
||||||
match handle_quest_entry(quest_entry) {
|
match handle_quest_entry(quest_entry) {
|
||||||
Ok(quest) => out_vec.push(quest),
|
Ok(quest) => out_vec.push(quest),
|
||||||
Err(error) => {
|
Err(error) if self.verbose => {
|
||||||
eprintln!("Error on loading single quest: {error}");
|
eprintln!("Error on loading single quest: {error}");
|
||||||
|
},
|
||||||
|
_ => {},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Err(error) if self.verbose => {
|
||||||
|
eprintln!("Error on loading single quest: {error}");
|
||||||
|
},
|
||||||
|
_ => {},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
Err(error) => {
|
Err(error) if self.verbose => {
|
||||||
eprintln!("Error on loading single quest: {error}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
Err(error) => {
|
|
||||||
eprintln!("Error on loading quests: {error}");
|
eprintln!("Error on loading quests: {error}");
|
||||||
}
|
},
|
||||||
|
_ => {},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if self.verbose {
|
||||||
println!("Loaded {} quests successfully", out_vec.len());
|
println!("Loaded {} quests successfully", out_vec.len());
|
||||||
|
}
|
||||||
|
|
||||||
out_vec
|
out_vec
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns full path to accounts folder
|
||||||
|
/// This path will be relative to $PWD, not to config.
|
||||||
|
///
|
||||||
|
/// # Examples
|
||||||
|
/// ```rust
|
||||||
|
/// use squad_quest::config::Config;
|
||||||
|
///
|
||||||
|
/// let path = "cfg/config.toml".into();
|
||||||
|
/// let config = Config::load(path);
|
||||||
|
///
|
||||||
|
/// let accounts_path = config.full_accounts_path();
|
||||||
|
/// ```
|
||||||
|
pub fn full_accounts_path(&self) -> PathBuf {
|
||||||
|
let mut path = self.path.clone();
|
||||||
|
path.push(self.accounts_path.clone());
|
||||||
|
path
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Load [Vec]<[Account]> from accounts folder.
|
||||||
|
/// Also logs errors and counts successfully loaded quests.
|
||||||
|
///
|
||||||
|
/// # Examples
|
||||||
|
/// ```rust
|
||||||
|
/// use squad_quest::{config::Config, account::Account};
|
||||||
|
///
|
||||||
|
///
|
||||||
|
/// let path = "cfg/config.toml".into();
|
||||||
|
/// let config = Config::load(path);
|
||||||
|
/// let accounts = config.load_accounts();
|
||||||
|
///
|
||||||
|
/// for account in accounts {
|
||||||
|
/// println!("Account {}", account.id);
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
pub fn load_accounts(&self) -> Vec<Account> {
|
||||||
|
let mut out_vec = Vec::new();
|
||||||
|
|
||||||
|
let path = self.full_accounts_path();
|
||||||
|
|
||||||
|
match fs::read_dir(path) {
|
||||||
|
Ok(iter) => {
|
||||||
|
for entry in iter {
|
||||||
|
match entry {
|
||||||
|
Ok(acc_entry) => {
|
||||||
|
match handle_account_entry(acc_entry) {
|
||||||
|
Ok(quest) => out_vec.push(quest),
|
||||||
|
Err(error) if self.verbose => {
|
||||||
|
eprintln!("Error on loading single account: {error}");
|
||||||
|
},
|
||||||
|
_ => {},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Err(error) if self.verbose => {
|
||||||
|
eprintln!("Error on loading single account: {error}");
|
||||||
|
},
|
||||||
|
_ => {},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Err(error) if self.verbose => {
|
||||||
|
eprintln!("Error on loading accounts: {error}");
|
||||||
|
},
|
||||||
|
_ => {},
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.verbose {
|
||||||
|
println!("Loaded {} accounts successfully", out_vec.len());
|
||||||
|
}
|
||||||
|
|
||||||
|
out_vec
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns full path to map.toml
|
||||||
|
/// This path will be relative to $PWD, not to config.
|
||||||
|
///
|
||||||
|
/// # Examples
|
||||||
|
/// ```rust
|
||||||
|
/// use squad_quest::{config::Config,error::Error,map::Map,SquadObject};
|
||||||
|
/// # fn main() {
|
||||||
|
/// # let _ = wrapper();
|
||||||
|
/// # }
|
||||||
|
/// # fn wrapper() -> Result<(),Error> {
|
||||||
|
///
|
||||||
|
/// let path = "cfg/config.toml".into();
|
||||||
|
/// let config = Config::load(path);
|
||||||
|
///
|
||||||
|
/// let map_path = config.full_map_path();
|
||||||
|
/// let map = Map::load(map_path)?;
|
||||||
|
/// # Ok(())
|
||||||
|
/// # }
|
||||||
|
/// ```
|
||||||
|
pub fn full_map_path(&self) -> PathBuf {
|
||||||
|
let mut path = self.path.clone();
|
||||||
|
path.push(self.map.clone());
|
||||||
|
path
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
66
src/error.rs
Normal file
66
src/error.rs
Normal file
|
|
@ -0,0 +1,66 @@
|
||||||
|
//! Module for handling crate errors
|
||||||
|
|
||||||
|
use std::{fmt, path::PathBuf};
|
||||||
|
|
||||||
|
/// Error struct
|
||||||
|
#[derive(Debug)]
|
||||||
|
#[non_exhaustive]
|
||||||
|
pub enum Error {
|
||||||
|
/// Given path is not a file
|
||||||
|
IsNotAFile(PathBuf),
|
||||||
|
/// std::io::Error happenned when loading
|
||||||
|
IoError(std::io::Error),
|
||||||
|
/// toml::ser::Error happened when loading
|
||||||
|
TomlSerializeError(toml::ser::Error),
|
||||||
|
/// toml::de::Error happened when loading
|
||||||
|
TomlDeserializeError(toml::de::Error),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for Error {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
match self {
|
||||||
|
Error::IsNotAFile(path) => write!(f, "{:?} is not a file", path),
|
||||||
|
Error::IoError(error) => write!(f, "io error: {error}"),
|
||||||
|
Error::TomlSerializeError(error) => write!(f, "serialize error: {error}"),
|
||||||
|
Error::TomlDeserializeError(error) => write!(f, "parse error: {error}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Error related to quest logic
|
||||||
|
#[derive(Debug)]
|
||||||
|
#[non_exhaustive]
|
||||||
|
pub enum QuestError {
|
||||||
|
/// Quest (self.0) is already completed for given account (self.1)
|
||||||
|
AlreadyCompleted(u16, String),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for QuestError {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
match self {
|
||||||
|
Self::AlreadyCompleted(quest_id, account_id) => write!(f, "quest #{quest_id} is already completed for account \"{account_id}\""),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Error related to map logic
|
||||||
|
#[derive(Debug)]
|
||||||
|
#[non_exhaustive]
|
||||||
|
pub enum MapError {
|
||||||
|
/// Room not found in map file
|
||||||
|
RoomNotFound(u16),
|
||||||
|
/// Room (self.0) is already unlocked on account (self.1)
|
||||||
|
RoomAlreadyUnlocked(u16, String),
|
||||||
|
/// Account (self.1) does not have much money (self.0)
|
||||||
|
InsufficientFunds(u16, String),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for MapError {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
match self {
|
||||||
|
Self::RoomNotFound(id) => write!(f, "could not find room #{id}"),
|
||||||
|
Self::RoomAlreadyUnlocked(room_id, account_id) => write!(f, "room #{room_id} is already unlocked on account \"{account_id}\""),
|
||||||
|
Self::InsufficientFunds(room_id, account_id) => write!(f, "account \"{account_id}\" does not have enough money to unlock room #{room_id}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
68
src/lib.rs
68
src/lib.rs
|
|
@ -2,7 +2,75 @@
|
||||||
|
|
||||||
#![warn(missing_docs)]
|
#![warn(missing_docs)]
|
||||||
|
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
use crate::error::Error;
|
||||||
|
|
||||||
pub mod account;
|
pub mod account;
|
||||||
pub mod config;
|
pub mod config;
|
||||||
|
pub mod error;
|
||||||
pub mod map;
|
pub mod map;
|
||||||
pub mod quest;
|
pub mod quest;
|
||||||
|
|
||||||
|
/// Trait for objects that are internally used in squad_quest.
|
||||||
|
/// Contains functions and methods to interact with file system.
|
||||||
|
/// Files are saved in TOML format.
|
||||||
|
pub trait SquadObject {
|
||||||
|
/// Parse SquadObject TOML or return error
|
||||||
|
///
|
||||||
|
/// # Examples
|
||||||
|
/// ```rust
|
||||||
|
/// use squad_quest::{SquadObject,error::Error,quest::Quest};
|
||||||
|
/// # fn main() {
|
||||||
|
/// # let _ = wrapper();
|
||||||
|
/// # }
|
||||||
|
///
|
||||||
|
/// # fn wrapper() -> Result<(), Error> {
|
||||||
|
/// let path = "quests/0.toml".into();
|
||||||
|
///
|
||||||
|
/// let quest = Quest::load(path)?;
|
||||||
|
/// #
|
||||||
|
/// # Ok(())
|
||||||
|
/// # }
|
||||||
|
/// ```
|
||||||
|
fn load(path: PathBuf) -> Result<Self, Error> where Self: Sized;
|
||||||
|
|
||||||
|
/// Check if given file is a SquadObject, then delete it or raise an error.
|
||||||
|
/// If file is not a quest, raises [Error::TomlDeserializeError]
|
||||||
|
///
|
||||||
|
/// # Examples
|
||||||
|
/// ```rust
|
||||||
|
/// use squad_quest::{SquadObject,error::Error,quest::Quest};
|
||||||
|
///
|
||||||
|
/// let path = "quests/0.toml".into();
|
||||||
|
///
|
||||||
|
/// if let Err(error) = Quest::delete(path) {
|
||||||
|
/// // handle the error
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
fn delete(path: PathBuf) -> Result<(), Error>;
|
||||||
|
|
||||||
|
/// Save SquadObject to given folder in TOML format.
|
||||||
|
/// File will be saved as `{id}.toml`.
|
||||||
|
/// If file exists, this method will override it.
|
||||||
|
///
|
||||||
|
/// # Examples
|
||||||
|
/// ```rust
|
||||||
|
/// # fn main() {
|
||||||
|
/// use squad_quest::{SquadObject,error::Error,quest::Quest};
|
||||||
|
/// use std::path::PathBuf;
|
||||||
|
///
|
||||||
|
/// let quest = Quest::default();
|
||||||
|
///
|
||||||
|
/// let path: PathBuf = "quests".into();
|
||||||
|
/// # let path2 = path.clone();
|
||||||
|
///
|
||||||
|
/// if let Err(error) = quest.save(path) {
|
||||||
|
/// // handle the error
|
||||||
|
/// }
|
||||||
|
/// # let filename = format!("{}.toml", quest.id);
|
||||||
|
/// # let _ = Quest::delete(path2.with_file_name(filename));
|
||||||
|
/// # }
|
||||||
|
/// ```
|
||||||
|
fn save(&self, path: PathBuf) -> Result<(), Error>;
|
||||||
|
}
|
||||||
|
|
|
||||||
144
src/map/mod.rs
144
src/map/mod.rs
|
|
@ -1,5 +1,143 @@
|
||||||
//! Map, a.k.a. a graph of rooms
|
//! Map, a.k.a. a graph of rooms
|
||||||
#![allow(dead_code)]
|
|
||||||
|
|
||||||
/// Graph for room nodes
|
use std::{fs, io::Write, path::PathBuf};
|
||||||
pub struct Map;
|
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
use crate::{SquadObject, account::Account, error::{Error, MapError}};
|
||||||
|
|
||||||
|
/// THE Graph. Actually, this is a Vec.
|
||||||
|
#[derive(Serialize, Deserialize)]
|
||||||
|
#[serde(default)]
|
||||||
|
pub struct Map {
|
||||||
|
/// Rooms go here
|
||||||
|
pub room: Vec<Room>
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Map {
|
||||||
|
fn default() -> Self {
|
||||||
|
Map { room: Vec::new() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SquadObject for Map {
|
||||||
|
fn load(path: PathBuf) -> Result<Self, Error> {
|
||||||
|
match std::fs::read_to_string(path) {
|
||||||
|
Ok(string) => {
|
||||||
|
match toml::from_str::<Self>(&string) {
|
||||||
|
Ok(object) => Ok(object),
|
||||||
|
Err(error) => Err(Error::TomlDeserializeError(error))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Err(error) => Err(Error::IoError(error))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn delete(path: PathBuf) -> Result<(), Error> {
|
||||||
|
match Self::load(path.clone()) {
|
||||||
|
Ok(_) => {
|
||||||
|
if let Err(error) = fs::remove_file(path) {
|
||||||
|
return Err(Error::IoError(error));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
},
|
||||||
|
Err(error) => Err(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn save(&self, path: PathBuf) -> Result<(), Error> {
|
||||||
|
let filename = "map.toml".to_string();
|
||||||
|
let mut full_path = path;
|
||||||
|
full_path.push(filename);
|
||||||
|
|
||||||
|
let str = match toml::to_string_pretty(&self) {
|
||||||
|
Ok(string) => string,
|
||||||
|
Err(error) => {
|
||||||
|
return Err(Error::TomlSerializeError(error));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut file = match fs::File::create(full_path) {
|
||||||
|
Ok(f) => f,
|
||||||
|
Err(error) => {
|
||||||
|
return Err(Error::IoError(error));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Err(error) = file.write_all(str.as_bytes()) {
|
||||||
|
return Err(Error::IoError(error));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Map {
|
||||||
|
/// Try to unlock room for account, or return [MapError]
|
||||||
|
///
|
||||||
|
/// # Examples
|
||||||
|
/// ```rust
|
||||||
|
/// use squad_quest::{account::Account,map::{Map,Room},error::MapError};
|
||||||
|
///
|
||||||
|
/// let map = Map {
|
||||||
|
/// room: vec![Room { id: 0, value: 100, ..Default::default() }],
|
||||||
|
/// };
|
||||||
|
///
|
||||||
|
/// let mut account = Account { balance: 100, ..Default::default() };
|
||||||
|
///
|
||||||
|
/// if let Err(error) = map.unlock_room_for_account(0, &mut account) {
|
||||||
|
/// // handle error
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
pub fn unlock_room_for_account(&self, room_id: u16, account: &mut Account) -> Result<(), MapError> {
|
||||||
|
let Some(room) = self.room.iter().find(|r| r.id == room_id) else {
|
||||||
|
return Err(MapError::RoomNotFound(room_id));
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(_) = account.rooms_unlocked.iter().find(|rid| **rid == room_id) {
|
||||||
|
return Err(MapError::RoomAlreadyUnlocked(room_id, account.id.clone()));
|
||||||
|
}
|
||||||
|
|
||||||
|
if account.balance < room.value {
|
||||||
|
return Err(MapError::InsufficientFunds(room_id, account.id.clone()));
|
||||||
|
}
|
||||||
|
|
||||||
|
account.balance -= room.value;
|
||||||
|
account.rooms_unlocked.push(room_id);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Component of the map
|
||||||
|
#[derive(Serialize, Deserialize, PartialEq, Debug)]
|
||||||
|
#[serde(default)]
|
||||||
|
pub struct Room {
|
||||||
|
/// Room id
|
||||||
|
pub id: u16,
|
||||||
|
/// Rooms that are connected with this
|
||||||
|
pub children: Vec<u16>,
|
||||||
|
/// Price of the room
|
||||||
|
pub value: u32,
|
||||||
|
/// Room name
|
||||||
|
pub name: String,
|
||||||
|
/// Room description
|
||||||
|
pub description: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn default_name() -> String {
|
||||||
|
"Hall".to_string()
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Room {
|
||||||
|
fn default() -> Self {
|
||||||
|
Room {
|
||||||
|
id: u16::default(),
|
||||||
|
children: Vec::new(),
|
||||||
|
value: u32::default(),
|
||||||
|
name: default_name(),
|
||||||
|
description: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,28 +0,0 @@
|
||||||
//! Module for handling quest loading errors
|
|
||||||
|
|
||||||
use std::{fmt, path::PathBuf};
|
|
||||||
|
|
||||||
/// Error raised when trying to parse quest file
|
|
||||||
#[derive(Debug)]
|
|
||||||
#[non_exhaustive]
|
|
||||||
pub enum QuestError {
|
|
||||||
/// Given path is not a file
|
|
||||||
IsNotAFile(PathBuf),
|
|
||||||
/// std::io::Error happenned when loading
|
|
||||||
IoError(std::io::Error),
|
|
||||||
/// toml::ser::Error happened when loading
|
|
||||||
TomlSerializeError(toml::ser::Error),
|
|
||||||
/// toml::de::Error happened when loading
|
|
||||||
TomlDeserializeError(toml::de::Error),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl fmt::Display for QuestError {
|
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
||||||
match self {
|
|
||||||
QuestError::IsNotAFile(path) => write!(f, "{:?} is not a file", path),
|
|
||||||
QuestError::IoError(error) => write!(f, "io error: {error}"),
|
|
||||||
QuestError::TomlSerializeError(error) => write!(f, "serialize error: {error}"),
|
|
||||||
QuestError::TomlDeserializeError(error) => write!(f, "parse error: {error}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
124
src/quest/mod.rs
124
src/quest/mod.rs
|
|
@ -1,11 +1,10 @@
|
||||||
//! Text-based quests and user solutions for them
|
//! Text-based quests and user solutions for them
|
||||||
|
|
||||||
pub mod error;
|
|
||||||
|
|
||||||
use std::{fs, io::Write, path::PathBuf};
|
use std::{fs, io::Write, path::PathBuf};
|
||||||
|
|
||||||
use serde::{ Serialize, Deserialize };
|
use serde::{ Serialize, Deserialize };
|
||||||
use error::QuestError;
|
use crate::{SquadObject, account::Account, error::{Error, QuestError}};
|
||||||
|
use toml::value::Date;
|
||||||
|
|
||||||
/// Difficulty of the quest
|
/// Difficulty of the quest
|
||||||
#[derive(Serialize, Deserialize, Debug, PartialEq, Clone, Copy)]
|
#[derive(Serialize, Deserialize, Debug, PartialEq, Clone, Copy)]
|
||||||
|
|
@ -59,6 +58,15 @@ pub struct Quest {
|
||||||
|
|
||||||
/// Quest answer, available for admins
|
/// Quest answer, available for admins
|
||||||
pub answer: String,
|
pub answer: String,
|
||||||
|
|
||||||
|
/// Is quest available for regular users
|
||||||
|
pub public: bool,
|
||||||
|
|
||||||
|
/// When quest becomes public
|
||||||
|
pub available_on: Option<Date>,
|
||||||
|
|
||||||
|
/// When quest expires
|
||||||
|
pub deadline: Option<Date>
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for Quest {
|
impl Default for Quest {
|
||||||
|
|
@ -69,59 +77,32 @@ impl Default for Quest {
|
||||||
reward: u32::default(),
|
reward: u32::default(),
|
||||||
name: default_name(),
|
name: default_name(),
|
||||||
description: default_description(),
|
description: default_description(),
|
||||||
answer: default_answer()
|
answer: default_answer(),
|
||||||
|
public: false,
|
||||||
|
available_on: None,
|
||||||
|
deadline: None
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Quest {
|
impl SquadObject for Quest {
|
||||||
/// Parse quest TOML or return error
|
fn load(path: PathBuf) -> Result<Self, Error> {
|
||||||
///
|
|
||||||
/// # Examples
|
|
||||||
/// ```rust
|
|
||||||
/// use squad_quest::quest::{Quest,error::QuestError};
|
|
||||||
/// # fn main() {
|
|
||||||
/// # let _ = wrapper();
|
|
||||||
/// # }
|
|
||||||
///
|
|
||||||
/// # fn wrapper() -> Result<(), QuestError> {
|
|
||||||
/// let path = "quests/0.toml".into();
|
|
||||||
///
|
|
||||||
/// let quest = Quest::load(path)?;
|
|
||||||
/// #
|
|
||||||
/// # Ok(())
|
|
||||||
/// # }
|
|
||||||
/// ```
|
|
||||||
pub fn load(path: PathBuf) -> Result<Self, QuestError> {
|
|
||||||
match std::fs::read_to_string(path) {
|
match std::fs::read_to_string(path) {
|
||||||
Ok(string) => {
|
Ok(string) => {
|
||||||
match toml::from_str::<Quest>(&string) {
|
match toml::from_str::<Self>(&string) {
|
||||||
Ok(quest) => Ok(quest),
|
Ok(object) => Ok(object),
|
||||||
Err(error) => Err(QuestError::TomlDeserializeError(error))
|
Err(error) => Err(Error::TomlDeserializeError(error))
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
Err(error) => Err(QuestError::IoError(error))
|
Err(error) => Err(Error::IoError(error))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Check if given file is a quest, then delete it or raise an error.
|
fn delete(path: PathBuf) -> Result<(), Error> {
|
||||||
/// If file is not a quest, raises [QuestError::TomlDeserializeError]
|
|
||||||
///
|
|
||||||
/// # Examples
|
|
||||||
/// ```rust
|
|
||||||
/// use squad_quest::quest::{Quest,error::QuestError};
|
|
||||||
///
|
|
||||||
/// let path = "quests/0.toml".into();
|
|
||||||
///
|
|
||||||
/// if let Err(error) = Quest::delete(path) {
|
|
||||||
/// // handle the error
|
|
||||||
/// }
|
|
||||||
/// ```
|
|
||||||
pub fn delete(path: PathBuf) -> Result<(), QuestError> {
|
|
||||||
match Quest::load(path.clone()) {
|
match Quest::load(path.clone()) {
|
||||||
Ok(_) => {
|
Ok(_) => {
|
||||||
if let Err(error) = fs::remove_file(path) {
|
if let Err(error) = fs::remove_file(path) {
|
||||||
return Err(QuestError::IoError(error));
|
return Err(Error::IoError(error));
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|
@ -130,29 +111,7 @@ impl Quest {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Save quest to given folder in TOML format.
|
fn save(&self, path: PathBuf) -> Result<(), Error> {
|
||||||
/// File will be saved as `{id}.toml`.
|
|
||||||
/// If file exists, this method will override it.
|
|
||||||
///
|
|
||||||
/// # Examples
|
|
||||||
/// ```rust
|
|
||||||
/// # fn main() {
|
|
||||||
/// use squad_quest::quest::{Quest,error::QuestError};
|
|
||||||
/// use std::path::PathBuf;
|
|
||||||
///
|
|
||||||
/// let quest = Quest::default();
|
|
||||||
///
|
|
||||||
/// let path: PathBuf = "quests".into();
|
|
||||||
/// # let path2 = path.clone();
|
|
||||||
///
|
|
||||||
/// if let Err(error) = quest.save(path) {
|
|
||||||
/// // handle the error
|
|
||||||
/// }
|
|
||||||
/// # let filename = format!("{}.toml", quest.id);
|
|
||||||
/// # let _ = Quest::delete(path2.with_file_name(filename));
|
|
||||||
/// # }
|
|
||||||
/// ```
|
|
||||||
pub fn save(&self, path: PathBuf) -> Result<(), QuestError> {
|
|
||||||
let filename = format!("{}.toml", self.id);
|
let filename = format!("{}.toml", self.id);
|
||||||
let mut full_path = path;
|
let mut full_path = path;
|
||||||
full_path.push(filename);
|
full_path.push(filename);
|
||||||
|
|
@ -160,21 +119,50 @@ impl Quest {
|
||||||
let str = match toml::to_string_pretty(&self) {
|
let str = match toml::to_string_pretty(&self) {
|
||||||
Ok(string) => string,
|
Ok(string) => string,
|
||||||
Err(error) => {
|
Err(error) => {
|
||||||
return Err(QuestError::TomlSerializeError(error));
|
return Err(Error::TomlSerializeError(error));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut file = match fs::File::create(full_path) {
|
let mut file = match fs::File::create(full_path) {
|
||||||
Ok(f) => f,
|
Ok(f) => f,
|
||||||
Err(error) => {
|
Err(error) => {
|
||||||
return Err(QuestError::IoError(error));
|
return Err(Error::IoError(error));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
if let Err(error) = file.write_all(str.as_bytes()) {
|
if let Err(error) = file.write_all(str.as_bytes()) {
|
||||||
return Err(QuestError::IoError(error));
|
return Err(Error::IoError(error));
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Quest {
|
||||||
|
/// Complete quest for account and add reward to it's balance.
|
||||||
|
/// Does nothing and returns [QuestError::AlreadyCompleted]
|
||||||
|
/// if it is already completed.
|
||||||
|
///
|
||||||
|
/// # Examples
|
||||||
|
///
|
||||||
|
/// ```rust
|
||||||
|
/// use squad_quest::{account::Account,quest::Quest};
|
||||||
|
///
|
||||||
|
/// let quest = Quest::default();
|
||||||
|
/// let mut account = Account::default();
|
||||||
|
///
|
||||||
|
/// if let Err(error) = quest.complete_for_account(&mut account) {
|
||||||
|
/// // handle error
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
pub fn complete_for_account(&self, account: &mut Account) -> Result<(),QuestError> {
|
||||||
|
match account.quests_completed.iter().find(|qid| **qid == self.id) {
|
||||||
|
Some(_) => Err(QuestError::AlreadyCompleted(self.id, account.id.clone())),
|
||||||
|
None => {
|
||||||
|
account.quests_completed.push(self.id);
|
||||||
|
account.balance += self.reward;
|
||||||
|
Ok(())
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
33
tests/io.rs
33
tests/io.rs
|
|
@ -1,5 +1,4 @@
|
||||||
use squad_quest::{config::Config,quest::{error::{QuestError}, Quest}};
|
use squad_quest::{SquadObject, account::Account, config::Config, quest::Quest};
|
||||||
use std::path::PathBuf;
|
|
||||||
|
|
||||||
const CONFIG_PATH: &str = "tests/io/config.toml";
|
const CONFIG_PATH: &str = "tests/io/config.toml";
|
||||||
|
|
||||||
|
|
@ -7,23 +6,35 @@ const CONFIG_PATH: &str = "tests/io/config.toml";
|
||||||
// and Quest::save can override files,
|
// and Quest::save can override files,
|
||||||
// so this test covers full quest CRUD
|
// so this test covers full quest CRUD
|
||||||
#[test]
|
#[test]
|
||||||
fn quest_crud() -> Result<(), QuestError> {
|
fn quest_crud() {
|
||||||
let config = Config::load(CONFIG_PATH.into());
|
let config = Config::load(CONFIG_PATH.into());
|
||||||
|
|
||||||
let mut quests_path = PathBuf::from(CONFIG_PATH).parent().unwrap().to_owned();
|
let mut quests_path = config.full_quests_path();
|
||||||
quests_path.push(config.quests_path);
|
|
||||||
|
|
||||||
let quest = Quest::default();
|
let quest = Quest::default();
|
||||||
|
|
||||||
println!("{:?}", quests_path.clone());
|
quest.save(quests_path.clone()).unwrap();
|
||||||
|
|
||||||
quest.save(quests_path.clone())?;
|
|
||||||
|
|
||||||
let filename = format!("{}.toml", quest.id);
|
let filename = format!("{}.toml", quest.id);
|
||||||
|
|
||||||
quests_path.push(filename);
|
quests_path.push(filename);
|
||||||
|
|
||||||
Quest::delete(quests_path)?;
|
Quest::delete(quests_path).unwrap();
|
||||||
|
}
|
||||||
Ok(())
|
|
||||||
|
#[test]
|
||||||
|
fn account_crud() {
|
||||||
|
let config = Config::load(CONFIG_PATH.into());
|
||||||
|
|
||||||
|
let mut accounts_path = config.full_accounts_path();
|
||||||
|
|
||||||
|
let account = Account::default();
|
||||||
|
|
||||||
|
account.save(accounts_path.clone()).unwrap();
|
||||||
|
|
||||||
|
let filename = format!("{}.toml", account.id);
|
||||||
|
|
||||||
|
accounts_path.push(filename);
|
||||||
|
|
||||||
|
Account::delete(accounts_path).unwrap();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
1
tests/io/accounts/.placeholder
Normal file
1
tests/io/accounts/.placeholder
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
Placeholder file for git
|
||||||
1
tests/io/map.toml
Normal file
1
tests/io/map.toml
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
room = []
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
use squad_quest::{config::Config, quest::Quest};
|
use squad_quest::{SquadObject, account::Account, config::Config, map::{Map, Room}, quest::Quest};
|
||||||
|
|
||||||
static CONFIG_PATH: &str = "./tests/main/config.toml";
|
static CONFIG_PATH: &str = "./tests/main/config.toml";
|
||||||
|
|
||||||
|
|
@ -35,8 +35,96 @@ fn quest_one() {
|
||||||
reward: 100,
|
reward: 100,
|
||||||
name: "Example easy quest".to_owned(),
|
name: "Example easy quest".to_owned(),
|
||||||
description: "Answer this quest without any attachments or comments".to_owned(),
|
description: "Answer this quest without any attachments or comments".to_owned(),
|
||||||
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,
|
||||||
|
available_on: None,
|
||||||
|
deadline: None
|
||||||
};
|
};
|
||||||
|
|
||||||
assert_eq!(*quest, expected);
|
assert_eq!(*quest, expected);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn load_accounts() {
|
||||||
|
let config = Config::load(CONFIG_PATH.into());
|
||||||
|
let accounts = config.load_accounts();
|
||||||
|
|
||||||
|
assert_eq!(accounts.len(), 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn empty_account_is_default() {
|
||||||
|
let config = Config::load(CONFIG_PATH.into());
|
||||||
|
let accounts = config.load_accounts();
|
||||||
|
|
||||||
|
let default = Account::default();
|
||||||
|
|
||||||
|
let account = accounts.iter().find(|a| a.id == default.id).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(*account, default);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn account_test() {
|
||||||
|
let config = Config::load(CONFIG_PATH.into());
|
||||||
|
|
||||||
|
let expected = Account {
|
||||||
|
id: "test".to_string(),
|
||||||
|
balance: 150,
|
||||||
|
location: 0,
|
||||||
|
quests_completed: vec![0],
|
||||||
|
rooms_unlocked: Vec::new()
|
||||||
|
};
|
||||||
|
|
||||||
|
let accounts = config.load_accounts();
|
||||||
|
let account = accounts.iter().find(|a| a.id == expected.id).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(*account, expected);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn load_map() {
|
||||||
|
let config = Config::load(CONFIG_PATH.into());
|
||||||
|
|
||||||
|
let room0 = Room {
|
||||||
|
id: 0,
|
||||||
|
children: vec![1, 2],
|
||||||
|
value: 0,
|
||||||
|
name: "Entrance".to_string(),
|
||||||
|
description: Some("Enter the dungeon".to_string()),
|
||||||
|
};
|
||||||
|
|
||||||
|
let room1 = Room {
|
||||||
|
id: 1,
|
||||||
|
children: vec![0, 3],
|
||||||
|
value: 100,
|
||||||
|
name: "Kitchen hall".to_string(),
|
||||||
|
description: None,
|
||||||
|
};
|
||||||
|
|
||||||
|
let room2 = Room {
|
||||||
|
id: 2,
|
||||||
|
children: vec![0],
|
||||||
|
value: 250,
|
||||||
|
name: "Room".to_string(),
|
||||||
|
description: Some("Simple room with no furniture".to_string()),
|
||||||
|
};
|
||||||
|
|
||||||
|
let room3 = Room {
|
||||||
|
id: 3,
|
||||||
|
children: vec![1],
|
||||||
|
value: 175,
|
||||||
|
name: "Kitchen".to_string(),
|
||||||
|
description: Some("Knives are stored here".to_string()),
|
||||||
|
};
|
||||||
|
|
||||||
|
let expected = Map {
|
||||||
|
room: vec![room0, room1, room2, room3],
|
||||||
|
};
|
||||||
|
|
||||||
|
let map_path = config.full_map_path();
|
||||||
|
|
||||||
|
let map = Map::load(map_path).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(map.room, expected.room);
|
||||||
|
}
|
||||||
|
|
|
||||||
1
tests/main/accounts/none.toml
Normal file
1
tests/main/accounts/none.toml
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
# Empty account for testing
|
||||||
5
tests/main/accounts/test.toml
Normal file
5
tests/main/accounts/test.toml
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
id = "test"
|
||||||
|
balance = 150
|
||||||
|
location = 0
|
||||||
|
quests_completed = [ 0 ]
|
||||||
|
rooms_unlocked = []
|
||||||
26
tests/main/map.toml
Normal file
26
tests/main/map.toml
Normal file
|
|
@ -0,0 +1,26 @@
|
||||||
|
[[room]]
|
||||||
|
id = 0
|
||||||
|
children = [ 1, 2 ]
|
||||||
|
value = 0
|
||||||
|
name = "Entrance"
|
||||||
|
description = "Enter the dungeon"
|
||||||
|
|
||||||
|
[[room]]
|
||||||
|
id = 1
|
||||||
|
children = [ 0, 3 ]
|
||||||
|
value = 100
|
||||||
|
name = "Kitchen hall"
|
||||||
|
|
||||||
|
[[room]]
|
||||||
|
id = 2
|
||||||
|
children = [ 0 ]
|
||||||
|
value = 250
|
||||||
|
name = "Room"
|
||||||
|
description = "Simple room with no furniture"
|
||||||
|
|
||||||
|
[[room]]
|
||||||
|
id = 3
|
||||||
|
children = [ 1 ]
|
||||||
|
value = 175
|
||||||
|
name = "Kitchen"
|
||||||
|
description = "Knives are stored here"
|
||||||
|
|
@ -6,3 +6,4 @@ reward = 100
|
||||||
name = "Example easy quest"
|
name = "Example easy quest"
|
||||||
description = "Answer this quest without any attachments or comments"
|
description = "Answer this quest without any attachments or comments"
|
||||||
answer = "Accept the answer if it has no attachments and an empty comment"
|
answer = "Accept the answer if it has no attachments and an empty comment"
|
||||||
|
public = false
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue