aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitignore1
-rw-r--r--Cargo.lock1360
-rw-r--r--Cargo.toml27
-rw-r--r--src/baas.rs214
-rw-r--r--src/contentdb.rs120
-rw-r--r--src/download.rs120
-rw-r--r--src/error.rs39
-rw-r--r--src/game.rs67
-rw-r--r--src/kvstore.rs49
-rw-r--r--src/lib.rs52
-rw-r--r--src/main.rs335
-rw-r--r--src/minemod.rs165
-rw-r--r--src/world.rs86
13 files changed, 2635 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..ea8c4bf
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1 @@
+/target
diff --git a/Cargo.lock b/Cargo.lock
new file mode 100644
index 0000000..bce8b00
--- /dev/null
+++ b/Cargo.lock
@@ -0,0 +1,1360 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 3
+
+[[package]]
+name = "adler"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe"
+
+[[package]]
+name = "aho-corasick"
+version = "0.7.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1e37cfd5e7657ada45f742d6e99ca5788580b5c529dc78faf11ece6dc702656f"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "ansi_term"
+version = "0.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ee49baf6cb617b853aa8d93bf420db2383fab46d314482ca2803b40d5fde979b"
+dependencies = [
+ "winapi",
+]
+
+[[package]]
+name = "anyhow"
+version = "1.0.45"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ee10e43ae4a853c0a3591d4e2ada1719e553be18199d9da9d4a83f5927c2f5c7"
+
+[[package]]
+name = "atty"
+version = "0.2.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8"
+dependencies = [
+ "hermit-abi",
+ "libc",
+ "winapi",
+]
+
+[[package]]
+name = "autocfg"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a"
+
+[[package]]
+name = "base64"
+version = "0.13.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd"
+
+[[package]]
+name = "bitflags"
+version = "1.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
+
+[[package]]
+name = "bumpalo"
+version = "3.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8f1e260c3a9040a7c19a12468758f4c16f31a81a1fe087482be9570ec864bb6c"
+
+[[package]]
+name = "byteorder"
+version = "1.4.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610"
+
+[[package]]
+name = "bzip2"
+version = "0.4.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6afcd980b5f3a45017c57e57a2fcccbb351cc43a356ce117ef760ef8052b89b0"
+dependencies = [
+ "bzip2-sys",
+ "libc",
+]
+
+[[package]]
+name = "bzip2-sys"
+version = "0.1.11+1.0.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "736a955f3fa7875102d57c82b8cac37ec45224a07fd32d58f9f7a186b6cd4cdc"
+dependencies = [
+ "cc",
+ "libc",
+ "pkg-config",
+]
+
+[[package]]
+name = "cc"
+version = "1.0.71"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "79c2681d6594606957bbb8631c4b90a7fcaaa72cdb714743a437b156d6a7eedd"
+
+[[package]]
+name = "cfg-if"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
+
+[[package]]
+name = "chrono"
+version = "0.4.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "670ad68c9088c2a963aaa298cb369688cf3f9465ce5e2d4ca10e6e0098a1ce73"
+dependencies = [
+ "libc",
+ "num-integer",
+ "num-traits",
+ "time",
+ "winapi",
+]
+
+[[package]]
+name = "chunked_transfer"
+version = "1.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fff857943da45f546682664a79488be82e69e43c1a7a2307679ab9afb3a66d2e"
+
+[[package]]
+name = "clap"
+version = "2.33.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "37e58ac78573c40708d45522f0d80fa2f01cc4f9b4e2bf749807255454312002"
+dependencies = [
+ "ansi_term",
+ "atty",
+ "bitflags",
+ "strsim",
+ "textwrap",
+ "unicode-width",
+ "vec_map",
+]
+
+[[package]]
+name = "convert_case"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e"
+
+[[package]]
+name = "crc32fast"
+version = "1.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "81156fece84ab6a9f2afdb109ce3ae577e42b1228441eded99bd77f627953b1a"
+dependencies = [
+ "cfg-if",
+]
+
+[[package]]
+name = "cssparser"
+version = "0.27.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "754b69d351cdc2d8ee09ae203db831e005560fc6030da058f86ad60c92a9cb0a"
+dependencies = [
+ "cssparser-macros",
+ "dtoa-short",
+ "itoa",
+ "matches",
+ "phf",
+ "proc-macro2",
+ "quote",
+ "smallvec",
+ "syn",
+]
+
+[[package]]
+name = "cssparser-macros"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dfae75de57f2b2e85e8768c3ea840fd159c8f33e2b6522c7835b7abac81be16e"
+dependencies = [
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "derive_more"
+version = "0.99.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "40eebddd2156ce1bb37b20bbe5151340a31828b1f2d22ba4141f3531710e38df"
+dependencies = [
+ "convert_case",
+ "proc-macro2",
+ "quote",
+ "rustc_version",
+ "syn",
+]
+
+[[package]]
+name = "dirs"
+version = "4.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ca3aa72a6f96ea37bbc5aa912f6788242832f75369bdfdadcb0e38423f100059"
+dependencies = [
+ "dirs-sys",
+]
+
+[[package]]
+name = "dirs-sys"
+version = "0.3.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "03d86534ed367a67548dc68113a0f5db55432fdfbb6e6f9d77704397d95d5780"
+dependencies = [
+ "libc",
+ "redox_users",
+ "winapi",
+]
+
+[[package]]
+name = "dtoa"
+version = "0.4.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "56899898ce76aaf4a0f24d914c97ea6ed976d42fec6ad33fcbb0a1103e07b2b0"
+
+[[package]]
+name = "dtoa-short"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bde03329ae10e79ede66c9ce4dc930aa8599043b0743008548680f25b91502d6"
+dependencies = [
+ "dtoa",
+]
+
+[[package]]
+name = "ego-tree"
+version = "0.6.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3a68a4904193147e0a8dec3314640e6db742afd5f6e634f428a6af230d9b3591"
+
+[[package]]
+name = "either"
+version = "1.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457"
+
+[[package]]
+name = "flate2"
+version = "1.0.22"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1e6988e897c1c9c485f43b47a529cef42fde0547f9d8d41a7062518f1d8fc53f"
+dependencies = [
+ "cfg-if",
+ "crc32fast",
+ "libc",
+ "miniz_oxide",
+]
+
+[[package]]
+name = "form_urlencoded"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5fc25a87fa4fd2094bffb06925852034d90a17f0d1e05197d4956d3555752191"
+dependencies = [
+ "matches",
+ "percent-encoding",
+]
+
+[[package]]
+name = "fs_extra"
+version = "1.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2022715d62ab30faffd124d40b76f4134a550a87792276512b18d63272333394"
+
+[[package]]
+name = "fuchsia-cprng"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a06f77d526c1a601b7c4cdd98f54b5eaabffc14d5f2f0296febdc7f357c6d3ba"
+
+[[package]]
+name = "futf"
+version = "0.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7c9c1ce3fa9336301af935ab852c437817d14cd33690446569392e65170aac3b"
+dependencies = [
+ "mac",
+ "new_debug_unreachable",
+]
+
+[[package]]
+name = "fxhash"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c31b6d751ae2c7f11320402d34e41349dd1016f8d5d45e48c4312bc8625af50c"
+dependencies = [
+ "byteorder",
+]
+
+[[package]]
+name = "getopts"
+version = "0.2.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "14dbbfd5c71d70241ecf9e6f13737f7b5ce823821063188d7e46c41d371eebd5"
+dependencies = [
+ "unicode-width",
+]
+
+[[package]]
+name = "getrandom"
+version = "0.1.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "wasi 0.9.0+wasi-snapshot-preview1",
+]
+
+[[package]]
+name = "getrandom"
+version = "0.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7fcd999463524c52659517fe2cea98493cfe485d10565e7b0fb07dbba7ad2753"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "wasi 0.10.0+wasi-snapshot-preview1",
+]
+
+[[package]]
+name = "hermit-abi"
+version = "0.1.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "html5ever"
+version = "0.25.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "aafcf38a1a36118242d29b92e1b08ef84e67e4a5ed06e0a80be20e6a32bfed6b"
+dependencies = [
+ "log",
+ "mac",
+ "markup5ever",
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "idna"
+version = "0.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "418a0a6fab821475f634efe3ccc45c013f742efe03d853e8d3355d5cb850ecf8"
+dependencies = [
+ "matches",
+ "unicode-bidi",
+ "unicode-normalization",
+]
+
+[[package]]
+name = "instant"
+version = "0.1.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c"
+dependencies = [
+ "cfg-if",
+]
+
+[[package]]
+name = "itertools"
+version = "0.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "69ddb889f9d0d08a67338271fa9b62996bc788c7796a5c18cf057420aaed5eaf"
+dependencies = [
+ "either",
+]
+
+[[package]]
+name = "itoa"
+version = "0.4.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b71991ff56294aa922b450139ee08b3bfc70982c6b2c7562771375cf73542dd4"
+
+[[package]]
+name = "js-sys"
+version = "0.3.55"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7cc9ffccd38c451a86bf13657df244e9c3f37493cce8e5e21e940963777acc84"
+dependencies = [
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "lazy_static"
+version = "1.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
+
+[[package]]
+name = "libc"
+version = "0.2.106"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a60553f9a9e039a333b4e9b20573b9e9b9c0bb3a11e201ccc48ef4283456d673"
+
+[[package]]
+name = "lock_api"
+version = "0.4.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "712a4d093c9976e24e7dbca41db895dabcbac38eb5f4045393d17a95bdfb1109"
+dependencies = [
+ "scopeguard",
+]
+
+[[package]]
+name = "log"
+version = "0.4.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "51b9bbe6c47d51fc3e1a9b945965946b4c44142ab8792c50835a980d362c2710"
+dependencies = [
+ "cfg-if",
+]
+
+[[package]]
+name = "mac"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4"
+
+[[package]]
+name = "markup5ever"
+version = "0.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a24f40fb03852d1cdd84330cddcaf98e9ec08a7b7768e952fad3b4cf048ec8fd"
+dependencies = [
+ "log",
+ "phf",
+ "phf_codegen",
+ "string_cache",
+ "string_cache_codegen",
+ "tendril",
+]
+
+[[package]]
+name = "matches"
+version = "0.1.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a3e378b66a060d48947b590737b30a1be76706c8dd7b8ba0f2fe3989c68a853f"
+
+[[package]]
+name = "memchr"
+version = "2.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "308cc39be01b73d0d18f82a0e7b2a3df85245f84af96fdddc5d202d27e47b86a"
+
+[[package]]
+name = "miniz_oxide"
+version = "0.4.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a92518e98c078586bc6c934028adcca4c92a53d6a958196de835170a01d84e4b"
+dependencies = [
+ "adler",
+ "autocfg",
+]
+
+[[package]]
+name = "modderbaas"
+version = "0.1.0"
+dependencies = [
+ "anyhow",
+ "clap",
+ "dirs",
+ "fs_extra",
+ "itertools",
+ "log",
+ "once_cell",
+ "regex",
+ "scraper",
+ "serde",
+ "stderrlog",
+ "tempdir",
+ "termcolor",
+ "thiserror",
+ "toml",
+ "ureq",
+ "url",
+ "zip",
+]
+
+[[package]]
+name = "new_debug_unreachable"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e4a24736216ec316047a1fc4252e27dabb04218aa4a3f37c6e7ddbf1f9782b54"
+
+[[package]]
+name = "nodrop"
+version = "0.1.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "72ef4a56884ca558e5ddb05a1d1e7e1bfd9a68d9ed024c21704cc98872dae1bb"
+
+[[package]]
+name = "num-integer"
+version = "0.1.44"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d2cc698a63b549a70bc047073d2949cce27cd1c7b0a4a862d08a8031bc2801db"
+dependencies = [
+ "autocfg",
+ "num-traits",
+]
+
+[[package]]
+name = "num-traits"
+version = "0.2.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9a64b1ec5cda2586e284722486d802acf1f7dbdc623e2bfc57e65ca1cd099290"
+dependencies = [
+ "autocfg",
+]
+
+[[package]]
+name = "once_cell"
+version = "1.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "692fcb63b64b1758029e0a96ee63e049ce8c5948587f2f7208df04625e5f6b56"
+
+[[package]]
+name = "parking_lot"
+version = "0.11.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7d17b78036a60663b797adeaee46f5c9dfebb86948d1255007a1d6be0271ff99"
+dependencies = [
+ "instant",
+ "lock_api",
+ "parking_lot_core",
+]
+
+[[package]]
+name = "parking_lot_core"
+version = "0.8.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d76e8e1493bcac0d2766c42737f34458f1c8c50c0d23bcb24ea953affb273216"
+dependencies = [
+ "cfg-if",
+ "instant",
+ "libc",
+ "redox_syscall",
+ "smallvec",
+ "winapi",
+]
+
+[[package]]
+name = "percent-encoding"
+version = "2.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e"
+
+[[package]]
+name = "pest"
+version = "2.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "10f4872ae94d7b90ae48754df22fd42ad52ce740b8f370b03da4835417403e53"
+dependencies = [
+ "ucd-trie",
+]
+
+[[package]]
+name = "phf"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3dfb61232e34fcb633f43d12c58f83c1df82962dcdfa565a4e866ffc17dafe12"
+dependencies = [
+ "phf_macros",
+ "phf_shared",
+ "proc-macro-hack",
+]
+
+[[package]]
+name = "phf_codegen"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cbffee61585b0411840d3ece935cce9cb6321f01c45477d30066498cd5e1a815"
+dependencies = [
+ "phf_generator",
+ "phf_shared",
+]
+
+[[package]]
+name = "phf_generator"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "17367f0cc86f2d25802b2c26ee58a7b23faeccf78a396094c13dced0d0182526"
+dependencies = [
+ "phf_shared",
+ "rand 0.7.3",
+]
+
+[[package]]
+name = "phf_macros"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7f6fde18ff429ffc8fe78e2bf7f8b7a5a5a6e2a8b58bc5a9ac69198bbda9189c"
+dependencies = [
+ "phf_generator",
+ "phf_shared",
+ "proc-macro-hack",
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "phf_shared"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c00cf8b9eafe68dde5e9eaa2cef8ee84a9336a47d566ec55ca16589633b65af7"
+dependencies = [
+ "siphasher",
+]
+
+[[package]]
+name = "pkg-config"
+version = "0.3.22"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "12295df4f294471248581bc09bef3c38a5e46f1e36d6a37353621a0c6c357e1f"
+
+[[package]]
+name = "ppv-lite86"
+version = "0.2.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ed0cfbc8191465bed66e1718596ee0b0b35d5ee1f41c5df2189d0fe8bde535ba"
+
+[[package]]
+name = "precomputed-hash"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c"
+
+[[package]]
+name = "proc-macro-hack"
+version = "0.5.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dbf0c48bc1d91375ae5c3cd81e3722dff1abcf81a30960240640d223f59fe0e5"
+
+[[package]]
+name = "proc-macro2"
+version = "1.0.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ba508cc11742c0dc5c1659771673afbab7a0efab23aa17e854cbab0837ed0b43"
+dependencies = [
+ "unicode-xid",
+]
+
+[[package]]
+name = "quote"
+version = "1.0.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "38bc8cc6a5f2e3655e0899c1b848643b2562f853f114bfec7be120678e3ace05"
+dependencies = [
+ "proc-macro2",
+]
+
+[[package]]
+name = "rand"
+version = "0.4.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "552840b97013b1a26992c11eac34bdd778e464601a4c2054b5f0bff7c6761293"
+dependencies = [
+ "fuchsia-cprng",
+ "libc",
+ "rand_core 0.3.1",
+ "rdrand",
+ "winapi",
+]
+
+[[package]]
+name = "rand"
+version = "0.7.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03"
+dependencies = [
+ "getrandom 0.1.16",
+ "libc",
+ "rand_chacha",
+ "rand_core 0.5.1",
+ "rand_hc",
+ "rand_pcg",
+]
+
+[[package]]
+name = "rand_chacha"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402"
+dependencies = [
+ "ppv-lite86",
+ "rand_core 0.5.1",
+]
+
+[[package]]
+name = "rand_core"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7a6fdeb83b075e8266dcc8762c22776f6877a63111121f5f8c7411e5be7eed4b"
+dependencies = [
+ "rand_core 0.4.2",
+]
+
+[[package]]
+name = "rand_core"
+version = "0.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9c33a3c44ca05fa6f1807d8e6743f3824e8509beca625669633be0acbdf509dc"
+
+[[package]]
+name = "rand_core"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19"
+dependencies = [
+ "getrandom 0.1.16",
+]
+
+[[package]]
+name = "rand_hc"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c"
+dependencies = [
+ "rand_core 0.5.1",
+]
+
+[[package]]
+name = "rand_pcg"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "16abd0c1b639e9eb4d7c50c0b8100b0d0f849be2349829c740fe8e6eb4816429"
+dependencies = [
+ "rand_core 0.5.1",
+]
+
+[[package]]
+name = "rdrand"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "678054eb77286b51581ba43620cc911abf02758c91f93f479767aed0f90458b2"
+dependencies = [
+ "rand_core 0.3.1",
+]
+
+[[package]]
+name = "redox_syscall"
+version = "0.2.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8383f39639269cde97d255a32bdb68c047337295414940c68bdd30c2e13203ff"
+dependencies = [
+ "bitflags",
+]
+
+[[package]]
+name = "redox_users"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "528532f3d801c87aec9def2add9ca802fe569e44a544afe633765267840abe64"
+dependencies = [
+ "getrandom 0.2.3",
+ "redox_syscall",
+]
+
+[[package]]
+name = "regex"
+version = "1.5.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d07a8629359eb56f1e2fb1652bb04212c072a87ba68546a04065d525673ac461"
+dependencies = [
+ "aho-corasick",
+ "memchr",
+ "regex-syntax",
+]
+
+[[package]]
+name = "regex-syntax"
+version = "0.6.25"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f497285884f3fcff424ffc933e56d7cbca511def0c9831a7f9b5f6153e3cc89b"
+
+[[package]]
+name = "remove_dir_all"
+version = "0.5.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7"
+dependencies = [
+ "winapi",
+]
+
+[[package]]
+name = "ring"
+version = "0.16.20"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3053cf52e236a3ed746dfc745aa9cacf1b791d846bdaf412f60a8d7d6e17c8fc"
+dependencies = [
+ "cc",
+ "libc",
+ "once_cell",
+ "spin",
+ "untrusted",
+ "web-sys",
+ "winapi",
+]
+
+[[package]]
+name = "rustc_version"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f0dfe2087c51c460008730de8b57e6a320782fbfb312e1f4d520e6c6fae155ee"
+dependencies = [
+ "semver",
+]
+
+[[package]]
+name = "rustls"
+version = "0.20.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9b5ac6078ca424dc1d3ae2328526a76787fecc7f8011f520e3276730e711fc95"
+dependencies = [
+ "log",
+ "ring",
+ "sct",
+ "webpki",
+]
+
+[[package]]
+name = "ryu"
+version = "1.0.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "71d301d4193d031abdd79ff7e3dd721168a9572ef3fe51a1517aba235bd8f86e"
+
+[[package]]
+name = "scopeguard"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd"
+
+[[package]]
+name = "scraper"
+version = "0.12.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "48e02aa790c80c2e494130dec6a522033b6a23603ffc06360e9fe6c611ea2c12"
+dependencies = [
+ "cssparser",
+ "ego-tree",
+ "getopts",
+ "html5ever",
+ "matches",
+ "selectors",
+ "smallvec",
+ "tendril",
+]
+
+[[package]]
+name = "sct"
+version = "0.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d53dcdb7c9f8158937a7981b48accfd39a43af418591a5d008c7b22b5e1b7ca4"
+dependencies = [
+ "ring",
+ "untrusted",
+]
+
+[[package]]
+name = "selectors"
+version = "0.22.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "df320f1889ac4ba6bc0cdc9c9af7af4bd64bb927bccdf32d81140dc1f9be12fe"
+dependencies = [
+ "bitflags",
+ "cssparser",
+ "derive_more",
+ "fxhash",
+ "log",
+ "matches",
+ "phf",
+ "phf_codegen",
+ "precomputed-hash",
+ "servo_arc",
+ "smallvec",
+ "thin-slice",
+]
+
+[[package]]
+name = "semver"
+version = "0.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f301af10236f6df4160f7c3f04eec6dbc70ace82d23326abad5edee88801c6b6"
+dependencies = [
+ "semver-parser",
+]
+
+[[package]]
+name = "semver-parser"
+version = "0.10.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "00b0bef5b7f9e0df16536d3961cfb6e84331c065b4066afb39768d0e319411f7"
+dependencies = [
+ "pest",
+]
+
+[[package]]
+name = "serde"
+version = "1.0.130"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f12d06de37cf59146fbdecab66aa99f9fe4f78722e3607577a5375d66bd0c913"
+dependencies = [
+ "serde_derive",
+]
+
+[[package]]
+name = "serde_derive"
+version = "1.0.130"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d7bc1a1ab1961464eae040d96713baa5a724a8152c1222492465b54322ec508b"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "serde_json"
+version = "1.0.68"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0f690853975602e1bfe1ccbf50504d67174e3bcf340f23b5ea9992e0587a52d8"
+dependencies = [
+ "itoa",
+ "ryu",
+ "serde",
+]
+
+[[package]]
+name = "servo_arc"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d98238b800e0d1576d8b6e3de32827c2d74bee68bb97748dcf5071fb53965432"
+dependencies = [
+ "nodrop",
+ "stable_deref_trait",
+]
+
+[[package]]
+name = "siphasher"
+version = "0.3.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "533494a8f9b724d33625ab53c6c4800f7cc445895924a8ef649222dcb76e938b"
+
+[[package]]
+name = "smallvec"
+version = "1.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1ecab6c735a6bb4139c0caafd0cc3635748bbb3acf4550e8138122099251f309"
+
+[[package]]
+name = "spin"
+version = "0.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d"
+
+[[package]]
+name = "stable_deref_trait"
+version = "1.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3"
+
+[[package]]
+name = "stderrlog"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "45a53e2eff3e94a019afa6265e8ee04cb05b9d33fe9f5078b14e4e391d155a38"
+dependencies = [
+ "atty",
+ "chrono",
+ "log",
+ "termcolor",
+ "thread_local",
+]
+
+[[package]]
+name = "string_cache"
+version = "0.8.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "923f0f39b6267d37d23ce71ae7235602134b250ace715dd2c90421998ddac0c6"
+dependencies = [
+ "lazy_static",
+ "new_debug_unreachable",
+ "parking_lot",
+ "phf_shared",
+ "precomputed-hash",
+ "serde",
+]
+
+[[package]]
+name = "string_cache_codegen"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f24c8e5e19d22a726626f1a5e16fe15b132dcf21d10177fa5a45ce7962996b97"
+dependencies = [
+ "phf_generator",
+ "phf_shared",
+ "proc-macro2",
+ "quote",
+]
+
+[[package]]
+name = "strsim"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a"
+
+[[package]]
+name = "syn"
+version = "1.0.81"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f2afee18b8beb5a596ecb4a2dce128c719b4ba399d34126b9e4396e3f9860966"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-xid",
+]
+
+[[package]]
+name = "tempdir"
+version = "0.3.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "15f2b5fb00ccdf689e0149d1b1b3c03fead81c2b37735d812fa8bddbbf41b6d8"
+dependencies = [
+ "rand 0.4.6",
+ "remove_dir_all",
+]
+
+[[package]]
+name = "tendril"
+version = "0.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a9ef557cb397a4f0a5a3a628f06515f78563f2209e64d47055d9dc6052bf5e33"
+dependencies = [
+ "futf",
+ "mac",
+ "utf-8",
+]
+
+[[package]]
+name = "termcolor"
+version = "1.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2dfed899f0eb03f32ee8c6a0aabdb8a7949659e3466561fc0adf54e26d88c5f4"
+dependencies = [
+ "winapi-util",
+]
+
+[[package]]
+name = "textwrap"
+version = "0.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060"
+dependencies = [
+ "unicode-width",
+]
+
+[[package]]
+name = "thin-slice"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8eaa81235c7058867fa8c0e7314f33dcce9c215f535d1913822a2b3f5e289f3c"
+
+[[package]]
+name = "thiserror"
+version = "1.0.30"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "854babe52e4df1653706b98fcfc05843010039b406875930a70e4d9644e5c417"
+dependencies = [
+ "thiserror-impl",
+]
+
+[[package]]
+name = "thiserror-impl"
+version = "1.0.30"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "aa32fd3f627f367fe16f893e2597ae3c05020f8bba2666a4e6ea73d377e5714b"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "thread_local"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d40c6d1b69745a6ec6fb1ca717914848da4b44ae29d9b3080cbee91d72a69b14"
+dependencies = [
+ "lazy_static",
+]
+
+[[package]]
+name = "time"
+version = "0.1.44"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6db9e6914ab8b1ae1c260a4ae7a49b6c5611b40328a735b21862567685e73255"
+dependencies = [
+ "libc",
+ "wasi 0.10.0+wasi-snapshot-preview1",
+ "winapi",
+]
+
+[[package]]
+name = "tinyvec"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f83b2a3d4d9091d0abd7eba4dc2710b1718583bd4d8992e2190720ea38f391f7"
+dependencies = [
+ "tinyvec_macros",
+]
+
+[[package]]
+name = "tinyvec_macros"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c"
+
+[[package]]
+name = "toml"
+version = "0.5.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a31142970826733df8241ef35dc040ef98c679ab14d7c3e54d827099b3acecaa"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "ucd-trie"
+version = "0.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "56dee185309b50d1f11bfedef0fe6d036842e3fb77413abef29f8f8d1c5d4c1c"
+
+[[package]]
+name = "unicode-bidi"
+version = "0.3.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1a01404663e3db436ed2746d9fefef640d868edae3cceb81c3b8d5732fda678f"
+
+[[package]]
+name = "unicode-normalization"
+version = "0.1.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d54590932941a9e9266f0832deed84ebe1bf2e4c9e4a3554d393d18f5e854bf9"
+dependencies = [
+ "tinyvec",
+]
+
+[[package]]
+name = "unicode-width"
+version = "0.1.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3ed742d4ea2bd1176e236172c8429aaf54486e7ac098db29ffe6529e0ce50973"
+
+[[package]]
+name = "unicode-xid"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3"
+
+[[package]]
+name = "untrusted"
+version = "0.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a"
+
+[[package]]
+name = "ureq"
+version = "2.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1dd912a3d096959150c4d71ac752e13f1683085922658c205b89b40fe8ebe07f"
+dependencies = [
+ "base64",
+ "chunked_transfer",
+ "log",
+ "once_cell",
+ "rustls",
+ "serde",
+ "serde_json",
+ "url",
+ "webpki",
+ "webpki-roots",
+]
+
+[[package]]
+name = "url"
+version = "2.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a507c383b2d33b5fc35d1861e77e6b383d158b2da5e14fe51b83dfedf6fd578c"
+dependencies = [
+ "form_urlencoded",
+ "idna",
+ "matches",
+ "percent-encoding",
+ "serde",
+]
+
+[[package]]
+name = "utf-8"
+version = "0.7.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9"
+
+[[package]]
+name = "vec_map"
+version = "0.8.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191"
+
+[[package]]
+name = "wasi"
+version = "0.9.0+wasi-snapshot-preview1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519"
+
+[[package]]
+name = "wasi"
+version = "0.10.0+wasi-snapshot-preview1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f"
+
+[[package]]
+name = "wasm-bindgen"
+version = "0.2.78"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "632f73e236b219150ea279196e54e610f5dbafa5d61786303d4da54f84e47fce"
+dependencies = [
+ "cfg-if",
+ "wasm-bindgen-macro",
+]
+
+[[package]]
+name = "wasm-bindgen-backend"
+version = "0.2.78"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a317bf8f9fba2476b4b2c85ef4c4af8ff39c3c7f0cdfeed4f82c34a880aa837b"
+dependencies = [
+ "bumpalo",
+ "lazy_static",
+ "log",
+ "proc-macro2",
+ "quote",
+ "syn",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-macro"
+version = "0.2.78"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d56146e7c495528bf6587663bea13a8eb588d39b36b679d83972e1a2dbbdacf9"
+dependencies = [
+ "quote",
+ "wasm-bindgen-macro-support",
+]
+
+[[package]]
+name = "wasm-bindgen-macro-support"
+version = "0.2.78"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7803e0eea25835f8abdc585cd3021b3deb11543c6fe226dcd30b228857c5c5ab"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+ "wasm-bindgen-backend",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-shared"
+version = "0.2.78"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0237232789cf037d5480773fe568aac745bfe2afbc11a863e97901780a6b47cc"
+
+[[package]]
+name = "web-sys"
+version = "0.3.55"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "38eb105f1c59d9eaa6b5cdc92b859d85b926e82cb2e0945cd0c9259faa6fe9fb"
+dependencies = [
+ "js-sys",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "webpki"
+version = "0.22.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f095d78192e208183081cc07bc5515ef55216397af48b873e5edcd72637fa1bd"
+dependencies = [
+ "ring",
+ "untrusted",
+]
+
+[[package]]
+name = "webpki-roots"
+version = "0.22.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c475786c6f47219345717a043a37ec04cb4bc185e28853adcc4fa0a947eba630"
+dependencies = [
+ "webpki",
+]
+
+[[package]]
+name = "winapi"
+version = "0.3.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
+dependencies = [
+ "winapi-i686-pc-windows-gnu",
+ "winapi-x86_64-pc-windows-gnu",
+]
+
+[[package]]
+name = "winapi-i686-pc-windows-gnu"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
+
+[[package]]
+name = "winapi-util"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178"
+dependencies = [
+ "winapi",
+]
+
+[[package]]
+name = "winapi-x86_64-pc-windows-gnu"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
+
+[[package]]
+name = "zip"
+version = "0.5.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "93ab48844d61251bb3835145c521d88aa4031d7139e8485990f60ca911fa0815"
+dependencies = [
+ "byteorder",
+ "bzip2",
+ "crc32fast",
+ "flate2",
+ "thiserror",
+ "time",
+]
diff --git a/Cargo.toml b/Cargo.toml
new file mode 100644
index 0000000..5bc82df
--- /dev/null
+++ b/Cargo.toml
@@ -0,0 +1,27 @@
+[package]
+name = "modderbaas"
+version = "0.1.0"
+authors = ["Daniel Schadt <kingdread@gmx.de>"]
+edition = "2018"
+
+# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
+
+[dependencies]
+anyhow = "1.0.45"
+clap = "2.33.3"
+dirs = "4.0.0"
+fs_extra = "1.2.0"
+itertools = "0.10.1"
+log = "0.4.14"
+once_cell = "1.8.0"
+regex = "1.5.4"
+scraper = "0.12.0"
+serde = { version = "1.0.130", features = ["derive"] }
+stderrlog = "0.5.1"
+tempdir = "0.3.7"
+termcolor = "1.1.2"
+thiserror = "1.0.30"
+toml = "0.5.8"
+ureq = { version = "2.3.0", features = ["json"] }
+url = { version = "2.2.2", features = ["serde"] }
+zip = "0.5.13"
diff --git a/src/baas.rs b/src/baas.rs
new file mode 100644
index 0000000..36a7aad
--- /dev/null
+++ b/src/baas.rs
@@ -0,0 +1,214 @@
+//! Functions to manipulate the global Minetest installation.
+use std::{collections::HashMap, path::PathBuf};
+
+use dirs;
+use log::debug;
+
+use super::{
+ error::Result,
+ game::Game,
+ minemod::{self, MineMod},
+ scan,
+ world::World,
+};
+
+/// Returns a list of folders in which worlds are expected.
+///
+/// Note that not all of these folders need to exist.
+///
+/// This returns the following locations:
+///
+/// * `$HOME/.minetest/worlds`
+/// * `/var/games/minetest-server/.minetest/worlds`
+pub fn world_dirs() -> Result<Vec<PathBuf>> {
+ let mut paths = vec!["/var/games/minetest-server/.minetest/worlds".into()];
+ if let Some(home) = dirs::home_dir() {
+ paths.push(home.join(".minetest").join("worlds"))
+ }
+ Ok(paths)
+}
+
+/// Returns a list of folders in which games are expected.
+///
+/// Note that not all of these folders need to exist.
+///
+/// This returns the following locations:
+///
+/// * `$HOME/.minetest/games`
+/// * `/var/games/minetest-server/.minetest/games`
+/// * `/usr/share/minetest/games`
+/// * `/usr/share/games/minetest/games`
+pub fn game_dirs() -> Result<Vec<PathBuf>> {
+ let mut paths = vec![
+ "/var/games/minetest-server/.minetest/games".into(),
+ "/usr/share/minetest/games".into(),
+ "/usr/share/games/minetest/games".into(),
+ ];
+ if let Some(home) = dirs::home_dir() {
+ paths.push(home.join(".minetest").join("games"))
+ }
+ Ok(paths)
+}
+
+/// Returns a list of folders in which mods are expected.
+///
+/// Note that not all of these folders need to exist.
+///
+/// This returns the following locations:
+///
+/// * `$HOME/.minetest/mods`
+/// * `/var/games/minetest-server/.minetest/mods`
+/// * `/usr/share/games/minetest/mods`
+/// * `/usr/share/minetest/mods`
+pub fn mod_dirs() -> Result<Vec<PathBuf>> {
+ let mut paths = vec![
+ "/var/games/minetest-server/.minetest/mods".into(),
+ "/usr/share/games/minetest/mods".into(),
+ "/usr/share/minetest/mods".into(),
+ ];
+ if let Some(home) = dirs::home_dir() {
+ paths.push(home.join(".minetest").join("mods"))
+ }
+ Ok(paths)
+}
+
+/// The [`Baas`] provides a way to list all worlds, games and mods on the system and allows access
+/// via the [`World`], [`Game`] and [`MineMod`] wrappers.
+#[derive(Debug, Default, Clone)]
+pub struct Baas {
+ world_dirs: Vec<PathBuf>,
+ game_dirs: Vec<PathBuf>,
+ mod_dirs: Vec<PathBuf>,
+}
+
+impl Baas {
+ /// Create a [`Baas`] with the standard dirs.
+ pub fn with_standard_dirs() -> Result<Baas> {
+ Ok(Baas::default()
+ .with_world_dirs(world_dirs()?)
+ .with_game_dirs(game_dirs()?)
+ .with_mod_dirs(mod_dirs()?))
+ }
+
+ /// Replace the world dirs with the given list of world dirs.
+ pub fn with_world_dirs(self, world_dirs: Vec<PathBuf>) -> Baas {
+ Baas { world_dirs, ..self }
+ }
+
+ /// The list of directories which are searched for worlds.
+ #[inline]
+ pub fn world_dirs(&self) -> &[PathBuf] {
+ self.world_dirs.as_slice()
+ }
+
+ /// Replace the game dirs with the given list of game dirs.
+ pub fn with_game_dirs(self, game_dirs: Vec<PathBuf>) -> Baas {
+ Baas { game_dirs, ..self }
+ }
+
+ /// The list of directories which are searched for games.
+ #[inline]
+ pub fn game_dirs(&self) -> &[PathBuf] {
+ self.game_dirs.as_slice()
+ }
+
+ /// Replace the mod dirs with the given list of mod dirs.
+ pub fn with_mod_dirs(self, mod_dirs: Vec<PathBuf>) -> Baas {
+ Baas { mod_dirs, ..self }
+ }
+
+ /// The list of directories which are searched for mods.
+ #[inline]
+ pub fn mod_dirs(&self) -> &[PathBuf] {
+ self.mod_dirs.as_slice()
+ }
+
+ /// Returns a vector of all words that were found in the world dirs.
+ pub fn worlds(&self) -> Result<Vec<World>> {
+ let mut worlds = vec![];
+ for dir in self.world_dirs() {
+ match scan(&dir, |p| World::open(p)) {
+ Ok(w) => worlds.extend(w),
+ Err(e) => debug!("Cannot scan {:?}: {}", dir, e),
+ }
+ }
+ Ok(worlds)
+ }
+
+ /// Returns a vector of all games that were found in the game dirs.
+ pub fn games(&self) -> Result<Vec<Game>> {
+ let mut games = vec![];
+ for dir in self.game_dirs() {
+ match scan(&dir, |p| Game::open(p)) {
+ Ok(g) => games.extend(g),
+ Err(e) => debug!("Cannot scan {:?}: {}", dir, e),
+ }
+ }
+ Ok(games)
+ }
+
+ /// Returns a vector of all mods that were found in the mod dirs.
+ ///
+ /// Note that modpacks are flattened into mods.
+ pub fn mods(&self) -> Result<Vec<MineMod>> {
+ let mut mods = vec![];
+ for dir in self.mod_dirs() {
+ match scan(&dir, |p| minemod::open_mod_or_pack(p)) {
+ Ok(m) => {
+ for container in m {
+ mods.extend(container.mods()?);
+ }
+ }
+ Err(e) => debug!("Cannot scan {:?}: {}", dir, e),
+ }
+ }
+ Ok(mods)
+ }
+
+ /// Return a snapshot of the current state.
+ ///
+ /// A snapshot "freezes" the lists of worlds, mods and games in time. It is useful to avoid
+ /// unnecessary I/O when it is known that the state should not have changed. It also allows
+ /// fast searching of items by their name.
+ pub fn snapshot(&self) -> Result<Snapshot> {
+ let worlds = self.worlds()?;
+ let games = self.games()?;
+ let mods = self.mods()?;
+
+ Ok(Snapshot {
+ worlds: worlds
+ .into_iter()
+ .map(|w| Ok((w.world_name()?, w)))
+ .collect::<Result<_>>()?,
+ games: games.into_iter().map(|g| (g.technical_name(), g)).collect(),
+ mods: mods
+ .into_iter()
+ .map(|m| Ok((m.mod_id()?, m)))
+ .collect::<Result<_>>()?,
+ })
+ }
+}
+
+/// Snapshot of a [`Baas`] scan.
+///
+/// Every item is indexed by its ID/name.
+#[derive(Debug, Clone)]
+pub struct Snapshot {
+ worlds: HashMap<String, World>,
+ games: HashMap<String, Game>,
+ mods: HashMap<String, MineMod>,
+}
+
+impl Snapshot {
+ pub fn worlds(&self) -> &HashMap<String, World> {
+ &self.worlds
+ }
+
+ pub fn games(&self) -> &HashMap<String, Game> {
+ &self.games
+ }
+
+ pub fn mods(&self) -> &HashMap<String, MineMod> {
+ &self.mods
+ }
+}
diff --git a/src/contentdb.rs b/src/contentdb.rs
new file mode 100644
index 0000000..467f9dc
--- /dev/null
+++ b/src/contentdb.rs
@@ -0,0 +1,120 @@
+//! Module to interact with the Minetest Content DB website.
+
+use once_cell::sync::Lazy;
+use scraper::{Html, Selector};
+use serde::{Deserialize, Serialize};
+use url::Url;
+
+use super::error::{Error, Result};
+
+/// The identification of content on Content DB. Consists of the username and the package name.
+pub type ContentId = (String, String);
+
+/// The URL of the default Content DB website to use.
+pub static DEFAULT_INSTANCE: Lazy<Url> =
+ Lazy::new(|| Url::parse("https://content.minetest.net/").expect("Invalid default URL"));
+
+/// The metapackage selector to scrape the packages.
+static PROVIDES_SELECTOR: Lazy<Selector> =
+ Lazy::new(|| Selector::parse("ul.d-flex").expect("Invalid selector"));
+
+static A_SELECTOR: Lazy<Selector> = Lazy::new(|| Selector::parse("a").expect("Invalid selector"));
+
+/// (Partial) metadata of a content item, as returned by the Content DB.
+#[derive(Debug, Clone, PartialEq, Eq, Hash, Deserialize, Serialize)]
+pub struct ContentMeta {
+ pub author: String,
+ pub name: String,
+ pub provides: Vec<String>,
+ pub short_description: String,
+ pub title: String,
+ #[serde(rename = "type")]
+ pub typ: String,
+ pub url: Url,
+}
+
+/// The main access point for Content DB queries.
+#[derive(Debug, Clone)]
+pub struct ContentDb {
+ base_url: Url,
+}
+
+impl Default for ContentDb {
+ fn default() -> Self {
+ Self::new()
+ }
+}
+
+impl ContentDb {
+ /// Create a new Content DB accessor pointing to the default instance.
+ pub fn new() -> ContentDb {
+ ContentDb {
+ base_url: DEFAULT_INSTANCE.clone(),
+ }
+ }
+
+ /// Find suitable candidates that provide the given modname.
+ pub fn resolve(&self, modname: &str) -> Result<Vec<ContentMeta>> {
+ let path = format!("metapackages/{}", modname);
+ let endpoint = self
+ .base_url
+ .join(&path)
+ .map_err(|_| Error::InvalidModId(modname.into()))?;
+
+ let body = ureq::request_url("GET", &endpoint).call()?.into_string()?;
+
+ let dom = Html::parse_document(&body);
+ let provides = dom
+ .select(&PROVIDES_SELECTOR)
+ .next()
+ .ok_or(Error::InvalidScrape)?;
+
+ let candidates: Vec<ContentId> = provides
+ .select(&A_SELECTOR)
+ .filter_map(|a| a.value().attr("href"))
+ .filter_map(extract_content_id)
+ .collect();
+
+ let mut good_ones = Vec::new();
+
+ for (user, package) in candidates {
+ let path = format!("api/packages/{}/{}/", user, package);
+ let endpoint = self
+ .base_url
+ .join(&path)
+ .expect("The parsed path was wrong");
+ let response: ContentMeta = ureq::request_url("GET", &endpoint).call()?.into_json()?;
+
+ // While resolving, we only care about actual mods that we can install. If a game
+ // provides a certain metapackage, it is pretty much useless for us (and often just
+ // there because a mod in that game provides the metapackage).
+ if response.typ == "mod" {
+ good_ones.push(response)
+ }
+ }
+
+ Ok(good_ones)
+ }
+
+ /// Retrieve the download url for a given package.
+ pub fn download_url(&self, user: &str, package: &str) -> Result<Url> {
+ let path = format!("api/packages/{}/{}/", user, package);
+ let endpoint = self
+ .base_url
+ .join(&path)
+ .expect("The parsed path was wrong");
+ let response: ContentMeta = ureq::request_url("GET", &endpoint).call()?.into_json()?;
+ Ok(response.url)
+ }
+}
+
+fn extract_content_id(path: &str) -> Option<ContentId> {
+ regex!("/packages/([^/]+)/([^/]+)/$")
+ .captures(path)
+ .map(|c| {
+ (
+ c.get(1).unwrap().as_str().into(),
+ c.get(2).unwrap().as_str().into(),
+ )
+ })
+}
diff --git a/src/download.rs b/src/download.rs
new file mode 100644
index 0000000..8a372ad
--- /dev/null
+++ b/src/download.rs
@@ -0,0 +1,120 @@
+use std::{
+ io::{Cursor, Read},
+ str::FromStr,
+};
+
+use tempdir::TempDir;
+use url::Url;
+use zip::ZipArchive;
+
+use super::{
+ contentdb::{ContentDb, ContentId},
+ error::{Error, Result},
+ minemod::{MineMod, ModId},
+};
+
+/// A source determines where a mod should be loaded from.
+#[derive(Debug, Clone, PartialEq, Eq, Hash)]
+pub enum Source {
+ /// Download a mod archive through HTTP.
+ Http(Url),
+ /// Download a mod from the Content DB, using the given user- and package name.
+ ContentDb(ContentId),
+ /// Search the Content DB for a given mod ID.
+ ///
+ /// The download may fail if there are multiple mods providing the same ID.
+ ModId(ModId),
+}
+
+impl FromStr for Source {
+ type Err = Error;
+
+ fn from_str(s: &str) -> Result<Self, Self::Err> {
+ if s.starts_with("http://") || s.starts_with("https://") {
+ let url = Url::parse(s)?;
+ return Ok(Source::Http(url));
+ }
+ let groups = regex!("^([^/]+)/([^/]+)$").captures(s);
+ if let Some(groups) = groups {
+ return Ok(Source::ContentDb((
+ groups.get(1).unwrap().as_str().into(),
+ groups.get(2).unwrap().as_str().into(),
+ )));
+ }
+
+ if !s.contains(' ') {
+ return Ok(Source::ModId(s.into()));
+ }
+
+ Err(Error::InvalidSourceSpec(s.into()))
+ }
+}
+
+/// A downloader is responsible for downloading mods from various sources.
+///
+/// Note that the [`MineMod`] that the [`Downloader`] creates will not work after the downloader
+/// has been destroyed, as the temporary files will be lost.
+#[derive(Debug)]
+pub struct Downloader {
+ temp_dir: TempDir,
+ content_db: ContentDb,
+}
+
+impl Downloader {
+ pub fn new() -> Result<Downloader> {
+ Downloader::with_content_db(Default::default())
+ }
+
+ pub fn with_content_db(content_db: ContentDb) -> Result<Downloader> {
+ let temp_dir = TempDir::new(env!("CARGO_PKG_NAME"))?;
+ Ok(Downloader {
+ temp_dir,
+ content_db,
+ })
+ }
+
+ pub fn download(&self, source: &Source) -> Result<MineMod> {
+ match *source {
+ Source::Http(ref url) => self.download_http(url),
+ Source::ContentDb((ref user, ref package)) => {
+ let url = self.content_db.download_url(user, package)?;
+ self.download_http(&url)
+ }
+ Source::ModId(ref id) => {
+ let candidates = self.content_db.resolve(id)?;
+ if candidates.len() != 1 {
+ return Err(Error::AmbiguousModId(id.into()));
+ }
+ self.download_http(&candidates[0].url)
+ }
+ }
+ }
+
+ /// Downloads a mod given a HTTP link.
+ ///
+ /// The [`Downloader`] expects to receive a zipfile containing the mod directory on this link.
+ ///
+ /// The mod is extracted to a temporary directory and has to be copied using
+ /// [`MineMod::copy_to`].
+ pub fn download_http(&self, url: &Url) -> Result<MineMod> {
+ let mut reader = ureq::request_url("GET", url).call()?.into_reader();
+ let mut data = Vec::new();
+ reader.read_to_end(&mut data)?;
+ let data = Cursor::new(data);
+ let mut archive = ZipArchive::new(data)?;
+
+ // Here we assume that the zipfile contains only one directory, so we just take the first
+ // name we find and extract the leading directory name.
+ let name = archive
+ .file_names()
+ .next()
+ .and_then(|name| name.split('/').next())
+ .ok_or(Error::EmptyArchive)?
+ .to_string();
+
+ archive.extract(self.temp_dir.path())?;
+
+ let extracted_path = self.temp_dir.path().join(name);
+ MineMod::open(&extracted_path)
+ }
+}
diff --git a/src/error.rs b/src/error.rs
new file mode 100644
index 0000000..d60fabd
--- /dev/null
+++ b/src/error.rs
@@ -0,0 +1,39 @@
+use std::path::PathBuf;
+
+use thiserror::Error;
+
+#[derive(Error, Debug)]
+pub enum Error {
+ #[error("invalid mod id '{0}'")]
+ InvalidModId(String),
+ #[error("underlying HTTP error")]
+ UreqError(#[from] ureq::Error),
+ #[error("underlying I/O error")]
+ IoError(#[from] std::io::Error),
+ #[error("the website returned unexpected data")]
+ InvalidScrape,
+ #[error("'{0}' is not a valid mod directory")]
+ InvalidModDir(PathBuf),
+ #[error("filesystem error")]
+ FsExtraError(#[from] fs_extra::error::Error),
+ #[error("ZIP error")]
+ ZipError(#[from] zip::result::ZipError),
+ #[error("the downloaded file was empty")]
+ EmptyArchive,
+ #[error("'{0}' is not a valid game directory")]
+ InvalidGameDir(PathBuf),
+ #[error("'{0}' is not a valid world directory")]
+ InvalidWorldDir(PathBuf),
+ #[error("'{0}' is not a valid modpack directory")]
+ InvalidModpackDir(PathBuf),
+ #[error("the world has no game ID set")]
+ NoGameSet,
+ #[error("'{0}' does not represent a valid mod source")]
+ InvalidSourceSpec(String),
+ #[error("invalid URL")]
+ UrlError(#[from] url::ParseError),
+ #[error("the mod ID '{0}' does not point to a single mod")]
+ AmbiguousModId(String),
+}
+
+pub type Result<T, E = Error> = std::result::Result<T, E>;
diff --git a/src/game.rs b/src/game.rs
new file mode 100644
index 0000000..f323212
--- /dev/null
+++ b/src/game.rs
@@ -0,0 +1,67 @@
+use std::{
+ fmt,
+ path::{Path, PathBuf},
+};
+
+use super::{
+ error::{Error, Result},
+ minemod::{self, MineMod},
+ scan,
+};
+
+/// Represents an on-disk Minetest game.
+#[derive(Debug, Clone, PartialEq, Eq, Hash)]
+pub struct Game {
+ path: PathBuf,
+}
+
+impl Game {
+ /// Open the given directory as a Minetest game.
+ ///
+ /// Note that the path may be relative, but only to the parent directory of the actual game.
+ /// This is because Minetest uses the game's directory name to identify the game
+ /// ([`Game::technical_name`]), so we need this information.
+ pub fn open<P: AsRef<Path>>(path: P) -> Result<Game> {
+ Game::open_path(path.as_ref())
+ }
+
+ fn open_path(path: &Path) -> Result<Game> {
+ if path.file_name().is_none() {
+ return Err(Error::InvalidGameDir(path.into()));
+ }
+ let conf = path.join("game.conf");
+ if !conf.is_file() {
+ return Err(Error::InvalidGameDir(path.into()));
+ }
+
+ Ok(Game { path: path.into() })
+ }
+
+ /// Returns the technical name of this game.
+ ///
+ /// This is the name that is used by minetest to identify the game.
+ pub fn technical_name(&self) -> String {
+ self.path
+ .file_name()
+ .expect("Somebody constructed an invalid `Game`")
+ .to_str()
+ .expect("Non-UTF8 directory encountered")
+ .into()
+ }
+
+ /// Returns all mods that this game provides.
+ pub fn mods(&self) -> Result<Vec<MineMod>> {
+ let path = self.path.join("mods");
+ let mut mods = vec![];
+ for container in scan(&path, |p| minemod::open_mod_or_pack(p))? {
+ mods.extend(container.mods()?);
+ }
+ Ok(mods)
+ }
+}
+
+impl fmt::Display for Game {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ write!(f, "{}", self.technical_name())
+ }
+}
diff --git a/src/kvstore.rs b/src/kvstore.rs
new file mode 100644
index 0000000..006bb94
--- /dev/null
+++ b/src/kvstore.rs
@@ -0,0 +1,49 @@
+//! Support module for writing `key=value` stores.
+//!
+//! These files are used by minetest mods (`mod.conf`), games (`game.conf`) and worlds
+//! (`world.mt`).
+//!
+//! Key-Value-Stores (KVStores) are represented by a [`std::collections::HashMap`] on the Rust
+//! side.
+use std::{collections::HashMap, fs, io::Write, path::Path};
+
+use super::error::Result;
+
+/// Read the given file as a KVStore.
+pub fn read<P: AsRef<Path>>(path: P) -> Result<HashMap<String, String>> {
+ read_inner(path.as_ref())
+}
+
+fn read_inner(path: &Path) -> Result<HashMap<String, String>> {
+ let content = fs::read_to_string(path)?;
+
+ Ok(content
+ .lines()
+ .map(|line| line.splitn(2, '=').map(str::trim).collect::<Vec<_>>())
+ .map(|v| (v[0].into(), v[1].into()))
+ .collect())
+}
+
+/// Write the given KVStore back to the file.
+///
+/// The order of the keys is guaranteed to be the following:
+///
+/// 1. All options that *don't* start with `"load_mod_"` are saved in alphabetical order.
+/// 2. All remaining options are saved in alphabetical order.
+///
+/// Note that this function will **override** existing files!
+pub fn write<P: AsRef<Path>>(data: &HashMap<String, String>, path: P) -> Result<()> {
+ write_inner(data, path.as_ref())
+}
+
+fn write_inner(data: &HashMap<String, String>, path: &Path) -> Result<()> {
+ let mut items = data.iter().collect::<Vec<_>>();
+ items.sort_by_key(|i| (if i.0.starts_with("load_mod_") { 1 } else { 0 }, i.0));
+
+ let mut output = fs::File::create(path)?;
+ for (key, value) in items {
+ writeln!(output, "{} = {}", key, value)?;
+ }
+
+ Ok(())
+}
diff --git a/src/lib.rs b/src/lib.rs
new file mode 100644
index 0000000..f169c82
--- /dev/null
+++ b/src/lib.rs
@@ -0,0 +1,52 @@
+use std::{fs, path::Path};
+
+use log::debug;
+
+macro_rules! regex {
+ ($re:literal $(,)?) => {{
+ static RE: once_cell::sync::OnceCell<regex::Regex> = once_cell::sync::OnceCell::new();
+ RE.get_or_init(|| regex::Regex::new($re).unwrap())
+ }};
+}
+
+pub mod baas;
+pub mod contentdb;
+pub mod download;
+pub mod error;
+pub mod game;
+pub mod kvstore;
+pub mod minemod;
+pub mod world;
+
+pub use baas::{Baas, Snapshot};
+pub use contentdb::ContentDb;
+pub use download::{Downloader, Source};
+pub use game::Game;
+pub use minemod::{MineMod, Modpack};
+pub use world::World;
+
+use error::Result;
+
+/// Scan all files in the given directory.
+///
+/// Files for which `scanner` returns `Ok(..)` will be collected and returned. Files for which
+/// `scanner` returns `Err(..)` will be silently discarded.
+///
+/// ```rust
+/// use modderbaas::minemod::MineMod;
+/// let mods = scan("/tmp", |p| MineMod::open(p))?;
+/// ```
+pub fn scan<W, P: AsRef<Path>, F: for<'p> Fn(&'p Path) -> Result<W>>(
+ path: P,
+ scanner: F,
+) -> Result<Vec<W>> {
+ debug!("Scanning through {:?}", path.as_ref());
+ let mut good_ones = vec![];
+ for entry in fs::read_dir(path)? {
+ let entry = entry?;
+ if let Ok(i) = scanner(&entry.path()) {
+ good_ones.push(i);
+ }
+ }
+ Ok(good_ones)
+}
diff --git a/src/main.rs b/src/main.rs
new file mode 100644
index 0000000..5df16a2
--- /dev/null
+++ b/src/main.rs
@@ -0,0 +1,335 @@
+use std::{
+ fmt::Display,
+ io::{self, Write},
+ path::Path,
+ str::FromStr,
+};
+
+use anyhow::{anyhow, bail, Context, Result};
+use clap::{crate_version, App, Arg, ArgMatches, SubCommand};
+use itertools::Itertools;
+use log::debug;
+use termcolor::{Color, ColorChoice, ColorSpec, StandardStream, WriteColor};
+
+use modderbaas::{Baas, ContentDb, Downloader, MineMod, Snapshot, Source, World};
+
+fn main() -> Result<()> {
+ stderrlog::new()
+ .module(module_path!())
+ //.verbosity(1)
+ .verbosity(5)
+ .init()
+ .unwrap();
+
+ let matches = App::new("ModderBaas")
+ .version(crate_version!())
+ .arg(
+ Arg::with_name("world")
+ .long("world")
+ .short("c")
+ .required(false)
+ .takes_value(true),
+ )
+ .subcommand(
+ SubCommand::with_name("enable")
+ .about("Enables a mod and its dependencies")
+ .arg(Arg::with_name("mod").multiple(true).required(true)),
+ )
+ .subcommand(
+ SubCommand::with_name("install")
+ .about("Installs a mod and its dependencies")
+ .arg(Arg::with_name("mod").multiple(true).required(true))
+ .arg(
+ Arg::with_name("target")
+ .short("t")
+ .long("target-dir")
+ .default_value("."),
+ ),
+ )
+ .get_matches();
+
+ let mut stdout = StandardStream::stdout(ColorChoice::Auto);
+
+ let baas = Baas::with_standard_dirs()?;
+ let snapshot = baas
+ .snapshot()
+ .context("Creating the initial snapshot failed")?;
+
+ let world = select_world(&mut stdout, &matches, &snapshot)?;
+ write!(stdout, "Using world ")?;
+ stdout.set_color(ColorSpec::new().set_fg(Some(Color::Green)))?;
+ writeln!(stdout, "{}", world.world_name()?)?;
+ stdout.reset()?;
+
+ if let Some(enable) = matches.subcommand_matches("enable") {
+ let mods = enable.values_of("mod").unwrap().collect::<Vec<_>>();
+ enable_mods(&mut stdout, &snapshot, &world, &mods)?;
+ }
+
+ if let Some(install) = matches.subcommand_matches("install") {
+ let mods = install.values_of("mod").unwrap().collect::<Vec<_>>();
+ let target_dir = install.value_of("target").unwrap();
+ install_mods(&mut stdout, &snapshot, &world, &mods, Path::new(target_dir))?;
+ }
+
+ Ok(())
+}
+
+/// Select the world that we want to work on.
+///
+/// If there is only one world available, we use it.
+/// If there are more worlds, we ask the user for a choice.
+///
+/// If the command line argument is given, it overrides the previous rules.
+fn select_world(
+ output: &mut StandardStream,
+ cli: &ArgMatches,
+ snapshot: &Snapshot,
+) -> Result<World> {
+ debug!("Starting world selection");
+ let worlds = snapshot.worlds();
+
+ if worlds.is_empty() {
+ bail!("No world found");
+ }
+
+ if let Some(world_name) = cli.value_of("world") {
+ if let Some(world) = worlds.get(world_name) {
+ return Ok(world.clone());
+ } else {
+ bail!("Invalid world name given: {}", world_name);
+ }
+ }
+
+ if worlds.len() == 1 {
+ return Ok(worlds
+ .values()
+ .next()
+ .expect("We just checked the length!")
+ .clone());
+ }
+
+ // Here, we cannot do an automatic selection, so ask the user:
+ let mut worlds = worlds.iter().collect::<Vec<_>>();
+ worlds.sort_by_key(|i| i.0);
+ writeln!(
+ output,
+ "The following worlds were found, please select one:"
+ )?;
+ user_choice(&worlds, output).map(|&i| i.clone())
+}
+
+/// Enables the given list of mods and their dependencies.
+///
+/// Fails if any mod has a dependency that can not be satisfied with the locally available mods.
+fn enable_mods(
+ output: &mut StandardStream,
+ snapshot: &Snapshot,
+ world: &World,
+ mods: &[&str],
+) -> Result<()> {
+ let mut wanted = mods.iter().map(|&s| s.to_owned()).collect::<Vec<String>>();
+ let mut to_enable = Vec::<MineMod>::new();
+ let game = snapshot
+ .games()
+ .get(&world.game_id()?)
+ .ok_or_else(|| anyhow!("The game definition was not found"))?;
+ let game_mods = game
+ .mods()?
+ .into_iter()
+ .map(|m| m.mod_id())
+ .collect::<Result<Vec<_>, _>>()?;
+
+ while !wanted.is_empty() {
+ let next_mod = wanted.remove(0);
+ // Do we already have the mod enabled, somehow?
+ if world.mods()?.contains(&next_mod) || game_mods.contains(&next_mod) {
+ continue;
+ }
+
+ match snapshot.mods().get(&next_mod as &str) {
+ Some(m) => {
+ to_enable.push(m.clone());
+ wanted.extend(m.dependencies()?);
+ }
+ None => bail!("Mod {} could not be found", next_mod),
+ }
+ }
+
+ if to_enable.is_empty() {
+ writeln!(output, "Done!")?;
+ return Ok(());
+ }
+
+ writeln!(output, "Enabling {} mods:", to_enable.len())?;
+ writeln!(output, "{}", to_enable.iter().join(", "))?;
+
+ ask_continue(output)?;
+
+ for m in to_enable {
+ let mod_id = m.mod_id()?;
+ world
+ .enable_mod(&mod_id)
+ .context(format!("Error enabling '{}'", mod_id))?;
+ }
+
+ writeln!(output, "Done!")?;
+ Ok(())
+}
+
+/// Install the given mods, installing dependencies if needed.
+fn install_mods(
+ output: &mut StandardStream,
+ snapshot: &Snapshot,
+ world: &World,
+ mods: &[&str],
+ target_dir: &Path,
+) -> Result<()> {
+ let content_db = ContentDb::new();
+ let downloader = Downloader::new()?;
+ let mut wanted = mods
+ .iter()
+ .map(|&s| Source::from_str(s))
+ .collect::<Result<Vec<_>, _>>()?;
+
+ let mut to_install = Vec::<MineMod>::new();
+ let mut to_enable = Vec::<MineMod>::new();
+
+ let game = snapshot
+ .games()
+ .get(&world.game_id()?)
+ .ok_or_else(|| anyhow!("The game definition was not found"))?;
+ let game_mods = game
+ .mods()?
+ .into_iter()
+ .map(|m| m.mod_id())
+ .collect::<Result<Vec<_>, _>>()?;
+
+ while !wanted.is_empty() {
+ let next_mod = wanted.remove(0);
+
+ // Special handling for mods specified by their ID, as those could already exist.
+ if let Source::ModId(ref id) = next_mod {
+ if let Some(m) = snapshot.mods().get(id) {
+ // We have that one, just enable it and its dependencies!
+ to_enable.push(m.clone());
+ wanted.extend(m.dependencies()?.into_iter().map(Source::ModId));
+ continue;
+ } else if game_mods.contains(id) {
+ // This mod is already contained in the game, nothing for us to do
+ continue;
+ }
+
+ // Is this a mod that is already queued for installation?
+ for m in &to_install {
+ if &m.mod_id()? == id {
+ continue;
+ }
+ }
+
+ // This mod is not available, so we search the content DB
+ writeln!(output, "Searching for candidates: {}", id)?;
+ let candidates = content_db.resolve(id)?;
+ if candidates.is_empty() {
+ bail!("Could not find a suitable mod for '{}'", id);
+ } else if candidates.len() == 1 {
+ wanted.push(Source::Http(candidates.into_iter().next().unwrap().url));
+ } else {
+ let items = candidates
+ .into_iter()
+ .map(|c| {
+ (
+ format!("{} by {} - {}", c.title, c.author, c.short_description),
+ c,
+ )
+ })
+ .collect::<Vec<_>>();
+ writeln!(
+ output,
+ "{} candidates found, please select one:",
+ items.len()
+ )?;
+ let candidate = user_choice(&items, output)?;
+ wanted.push(Source::Http(candidate.url.clone()));
+ }
+ } else {
+ let downloaded = downloader
+ .download(&next_mod)
+ .context("Failed to download mod")?;
+ wanted.extend(downloaded.dependencies()?.into_iter().map(Source::ModId));
+ to_install.push(downloaded);
+ }
+ }
+
+ writeln!(output, "Installing {} new mods:", to_install.len())?;
+ writeln!(output, "{}", to_install.iter().join(", "))?;
+
+ ask_continue(output)?;
+
+ for m in to_install {
+ let mod_id = m.mod_id()?;
+ writeln!(output, "Installing {}", mod_id)?;
+ let installed = m
+ .copy_to(target_dir)
+ .context(format!("Error installing '{}'", mod_id))?;
+ to_enable.push(installed);
+ }
+
+ for m in to_enable {
+ let mod_id = m.mod_id()?;
+ world
+ .enable_mod(&mod_id)
+ .context(format!("Error enabling '{}'", mod_id))?;
+ }
+
+ writeln!(output, "Done!")?;
+
+ Ok(())
+}
+
+/// Presents the user with a choice of items and awaits a selection.
+fn user_choice<'i, L: Display, I>(
+ items: &'i [(L, I)],
+ output: &mut StandardStream,
+) -> Result<&'i I> {
+ for (i, (label, _)) in items.iter().enumerate() {
+ output.set_color(ColorSpec::new().set_fg(Some(Color::Blue)))?;
+ write!(output, "[{}]", i)?;
+ output.reset()?;
+ writeln!(output, " {}", label)?;
+ }
+
+ let stdin = io::stdin();
+ loop {
+ write!(output, "Enter a number: ")?;
+ output.flush()?;
+ let mut buffer = String::new();
+ stdin.read_line(&mut buffer)?;
+ if let Ok(number) = buffer.trim().parse::<usize>() {
+ if number < items.len() {
+ return Ok(&items[number].1);
+ }
+ }
+ }
+}
+
+/// Ask the user whether they want to continue.
+///
+/// Returns `Ok(())` if the program should continue, and an error otherwise.
+fn ask_continue(output: &mut StandardStream) -> Result<()> {
+ let stdin = io::stdin();
+ loop {
+ output.set_color(ColorSpec::new().set_fg(Some(Color::Yellow)))?;
+ write!(output, "Continue? [Y/n] ")?;
+ output.reset()?;
+ output.flush()?;
+
+ let mut buffer = String::new();
+ stdin.read_line(&mut buffer)?;
+ if buffer == "\n" || buffer == "Y\n" || buffer == "y\n" {
+ return Ok(());
+ } else if buffer == "N\n" || buffer == "n\n" {
+ bail!("Cancelled by user");
+ }
+ }
+}
diff --git a/src/minemod.rs b/src/minemod.rs
new file mode 100644
index 0000000..6af50e3
--- /dev/null
+++ b/src/minemod.rs
@@ -0,0 +1,165 @@
+use std::{
+ any::Any,
+ collections::HashMap,
+ fmt, fs,
+ path::{Path, PathBuf},
+};
+
+use fs_extra::dir::{self, CopyOptions};
+
+use super::{
+ error::{Error, Result},
+ kvstore, scan,
+};
+
+/// The type of the ID that is used to identify Minetest mods.
+pub type ModId = String;
+
+/// A minemod is a mod that is saved somewhere on disk.
+#[derive(Debug, Clone, PartialEq, Eq, Hash)]
+pub struct MineMod {
+ path: PathBuf,
+}
+
+impl MineMod {
+ pub fn open<P: AsRef<Path>>(path: P) -> Result<MineMod> {
+ MineMod::open_path(path.as_ref())
+ }
+
+ fn open_path(path: &Path) -> Result<MineMod> {
+ let conf = path.join("mod.conf");
+ if !conf.is_file() {
+ return Err(Error::InvalidModDir(path.into()));
+ }
+
+ Ok(MineMod { path: path.into() })
+ }
+
+ fn read_conf(&self) -> Result<HashMap<String, String>> {
+ let conf = self.path.join("mod.conf");
+ kvstore::read(&conf)
+ }
+
+ /// Read the mod ID.
+ pub fn mod_id(&self) -> Result<ModId> {
+ let conf = self.read_conf()?;
+ conf.get("name")
+ .map(Into::into)
+ .ok_or_else(|| Error::InvalidModDir(self.path.clone()))
+ }
+
+ /// Returns all dependencies of this mod.
+ pub fn dependencies(&self) -> Result<Vec<ModId>> {
+ let conf = self.read_conf()?;
+ static EMPTY: String = String::new();
+ let depstr = conf.get("depends").unwrap_or(&EMPTY);
+ Ok(depstr
+ .split(',')
+ .map(str::trim)
+ .filter(|s| !s.is_empty())
+ .map(Into::into)
+ .collect())
+ }
+
+ /// Copies the mod to the given path.
+ ///
+ /// Note that the path should not include the mod directory, that will be appended
+ /// automatically.
+ ///
+ /// Returns a new [`MineMod`] object pointing to the copy.
+ pub fn copy_to<P: AsRef<Path>>(&self, path: P) -> Result<MineMod> {
+ let mut options = CopyOptions::new();
+ options.content_only = true;
+ let path = path.as_ref().join(self.mod_id()?);
+ fs::create_dir_all(&path)?;
+ dir::copy(&self.path, &path, &options)?;
+ MineMod::open(&path)
+ }
+}
+
+impl fmt::Display for MineMod {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ write!(f, "{}", self.mod_id().map_err(|_| fmt::Error)?)
+ }
+}
+
+/// Represents an on-disk modpack.
+///
+/// We don't support many modpack operations besides listing the modpack contents.
+#[derive(Debug, Clone, PartialEq, Eq, Hash)]
+pub struct Modpack {
+ path: PathBuf,
+}
+
+impl Modpack {
+ pub fn open<P: AsRef<Path>>(path: P) -> Result<Modpack> {
+ Modpack::open_path(path.as_ref())
+ }
+
+ fn open_path(path: &Path) -> Result<Modpack> {
+ let conf = path.join("modpack.conf");
+ if !conf.is_file() {
+ return Err(Error::InvalidModpackDir(path.into()));
+ }
+
+ Ok(Modpack { path: path.into() })
+ }
+
+ fn conf(&self) -> Result<HashMap<String, String>> {
+ let conf = self.path.join("modpack.conf");
+ kvstore::read(&conf)
+ }
+
+ /// Returns the name of the modpack.
+ pub fn name(&self) -> Result<String> {
+ self.conf()?
+ .get("name")
+ .map(Into::into)
+ .ok_or_else(|| Error::InvalidModDir(self.path.clone()))
+ }
+
+ /// Return all mods contained in this modpack.
+ pub fn mods(&self) -> Result<Vec<MineMod>> {
+ let mut mods = vec![];
+ for container in scan(&self.path, |p| open_mod_or_pack(p))? {
+ mods.extend(container.mods()?);
+ }
+ Ok(mods)
+ }
+}
+
+/// A thing that can contain mods.
+pub trait ModContainer: Any {
+ /// Returns the name of the mod container.
+ fn name(&self) -> Result<String>;
+
+ /// Return all contained mods.
+ fn mods(&self) -> Result<Vec<MineMod>>;
+}
+
+impl ModContainer for MineMod {
+ fn name(&self) -> Result<String> {
+ self.mod_id()
+ }
+
+ fn mods(&self) -> Result<Vec<MineMod>> {
+ Ok(vec![self.clone()])
+ }
+}
+
+impl ModContainer for Modpack {
+ fn name(&self) -> Result<String> {
+ self.name()
+ }
+
+ fn mods(&self) -> Result<Vec<MineMod>> {
+ self.mods()
+ }
+}
+
+/// Attempts to open the given path as either a single mod or a modpack.
+pub fn open_mod_or_pack<P: AsRef<Path>>(path: P) -> Result<Box<dyn ModContainer>> {
+ MineMod::open(path.as_ref())
+ .map(|m| Box::new(m) as Box<dyn ModContainer>)
+ .or_else(|_| Modpack::open(path.as_ref()).map(|p| Box::new(p) as Box<dyn ModContainer>))
+}
diff --git a/src/world.rs b/src/world.rs
new file mode 100644
index 0000000..42a41a4
--- /dev/null
+++ b/src/world.rs
@@ -0,0 +1,86 @@
+use std::{
+ collections::HashMap,
+ fmt,
+ path::{Path, PathBuf},
+};
+
+use super::{
+ error::{Error, Result},
+ kvstore,
+ minemod::ModId,
+};
+
+/// Represents an on-disk Minetest world.
+#[derive(Debug, Clone, PartialEq, Eq, Hash)]
+pub struct World {
+ path: PathBuf,
+}
+
+impl World {
+ pub fn open<P: AsRef<Path>>(path: P) -> Result<World> {
+ World::open_path(path.as_ref())
+ }
+
+ fn open_path(path: &Path) -> Result<World> {
+ let conf = path.join("world.mt");
+ if !conf.is_file() {
+ return Err(Error::InvalidWorldDir(path.into()));
+ }
+
+ Ok(World { path: path.into() })
+ }
+
+ fn conf(&self) -> Result<HashMap<String, String>> {
+ let conf = self.path.join("world.mt");
+ kvstore::read(&conf)
+ }
+
+ /// Returns the name of the world.
+ pub fn world_name(&self) -> Result<String> {
+ let conf = self.conf()?;
+ conf.get("world_name")
+ .ok_or_else(|| Error::InvalidWorldDir(self.path.clone()))
+ .map(Into::into)
+ }
+
+ /// Extract the game that this world uses.
+ pub fn game_id(&self) -> Result<String> {
+ let conf = self.conf()?;
+ conf.get("gameid").ok_or(Error::NoGameSet).map(Into::into)
+ }
+
+ /// Returns all mods that are loaded in this world.
+ ///
+ /// This returns mods that are explicitely loaded in the config, but not mods that are loaded
+ /// through the game.
+ pub fn mods(&self) -> Result<Vec<ModId>> {
+ let conf = self.conf()?;
+ const PREFIX_LEN: usize = "load_mod_".len();
+ Ok(conf
+ .iter()
+ .filter(|(k, _)| k.starts_with("load_mod_"))
+ .filter(|(_, v)| *v == "true")
+ .map(|i| i.0[PREFIX_LEN..].into())
+ .collect())
+ }
+
+ /// Enable the given mod.
+ ///
+ /// Note that this function does not ensure that the mod exists on-disk, nor does it do any
+ /// dependency checks. It simply adds the right `load_mod`-line to the world configuration
+ /// file.
+ pub fn enable_mod(&self, mod_id: &str) -> Result<()> {
+ let mut conf = self.conf()?;
+ let key = format!("load_mod_{}", mod_id);
+ conf.entry(key)
+ .and_modify(|e| *e = "true".into())
+ .or_insert_with(|| "true".into());
+ kvstore::write(&conf, &self.path.join("world.mt"))
+ }
+}
+
+impl fmt::Display for World {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ write!(f, "{}", self.world_name().map_err(|_| fmt::Error)?)
+ }
+}