diff options
-rw-r--r-- | .gitignore | 1 | ||||
-rw-r--r-- | Cargo.lock | 1360 | ||||
-rw-r--r-- | Cargo.toml | 27 | ||||
-rw-r--r-- | src/baas.rs | 214 | ||||
-rw-r--r-- | src/contentdb.rs | 120 | ||||
-rw-r--r-- | src/download.rs | 120 | ||||
-rw-r--r-- | src/error.rs | 39 | ||||
-rw-r--r-- | src/game.rs | 67 | ||||
-rw-r--r-- | src/kvstore.rs | 49 | ||||
-rw-r--r-- | src/lib.rs | 52 | ||||
-rw-r--r-- | src/main.rs | 335 | ||||
-rw-r--r-- | src/minemod.rs | 165 | ||||
-rw-r--r-- | src/world.rs | 86 |
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)?) + } +} |