├── .dockerignore ├── .gitattributes ├── .github └── workflows │ ├── build.yml │ ├── publish.yml │ └── release.yml ├── .gitignore ├── CATALOG.md ├── CHANGELOG.md ├── Cargo.lock ├── Cargo.toml ├── Dockerfile ├── LICENSE ├── README.md ├── docs ├── commands │ ├── build.md │ ├── cache.md │ ├── dev.md │ ├── env.md │ ├── export.md │ ├── import.md │ ├── index.md │ ├── info.md │ ├── init.md │ ├── markdown.md │ ├── pull.md │ ├── run.md │ ├── version.md │ └── world.md ├── concepts │ ├── building.md │ ├── caching.md │ ├── dev.md │ ├── getting-started.md │ ├── importing-modpacks.md │ ├── network.md │ ├── options.md │ ├── using-worlds.md │ └── variables.md ├── diagrams │ ├── build.drawio │ ├── build.png │ ├── diag1.drawio │ └── diag1.png ├── index.md ├── installation.md ├── mcman-3.png └── reference │ ├── clientsidemod.md │ ├── downloadable │ ├── curserinth.md │ ├── custom-url.md │ ├── github-releases.md │ ├── hangar.md │ ├── index.md │ ├── jenkins.md │ ├── maven.md │ ├── modrinth.md │ └── spigot.md │ ├── hook.md │ ├── lockfile.md │ ├── markdown-options.md │ ├── network.toml.md │ ├── server-launcher.md │ ├── server.toml.md │ ├── servertype │ ├── buildtools.md │ ├── bungeecord.md │ ├── fabric.md │ ├── forge.md │ ├── index.md │ ├── neoforge.md │ ├── papermc.md │ ├── purpurmc.md │ ├── quilt.md │ └── vanilla.md │ └── world.md ├── examples ├── creative-anarchy │ ├── README.md │ ├── config │ │ ├── server.properties │ │ └── spigot.yml │ ├── hotreload.toml │ ├── server.toml │ └── worlds │ │ └── world.zip ├── datapacks │ ├── README.md │ ├── config │ │ └── server.properties │ └── server.toml ├── forge │ ├── README.md │ ├── config │ │ └── server.properties │ └── server.toml ├── mrpack │ ├── README.md │ ├── config │ │ ├── config │ │ │ └── yosbr │ │ │ │ └── config │ │ │ │ ├── c2me.toml │ │ │ │ ├── ferritecore.mixin.properties │ │ │ │ ├── servercore.toml │ │ │ │ ├── threadtweak.json │ │ │ │ └── vmp.properties │ │ └── server.properties │ └── server.toml ├── neoforge │ ├── README.md │ ├── config │ │ └── server.properties │ └── server.toml ├── network │ ├── README.md │ ├── network.toml │ └── servers │ │ ├── game1 │ │ ├── README.md │ │ ├── config │ │ │ └── server.properties │ │ └── server.toml │ │ ├── lobby │ │ ├── README.md │ │ ├── config │ │ │ └── server.properties │ │ └── server.toml │ │ └── proxy │ │ ├── README.md │ │ ├── config │ │ └── velocity.toml │ │ └── server.toml ├── quilt │ ├── README.md │ ├── config │ │ └── server.properties │ ├── hotreload.toml │ ├── pack │ │ ├── index.toml │ │ ├── mods │ │ │ ├── LuckPerms-Fabric-5.4.111.pw.toml │ │ │ ├── LuckPerms-Fabric-5.4.113.pw.toml │ │ │ ├── create-fabric-0.5.1-b-build.1089+mc1.19.2.pw.toml │ │ │ ├── qfapi-4.0.0-beta.30_qsl-3.0.0-beta.29_fapi-0.76.0_mc-1.19.2.pw.toml │ │ │ └── spark-1.10.37-fabric.pw.toml │ │ ├── pack.toml │ │ └── server.properties │ └── server.toml ├── spigot │ ├── README.md │ ├── config │ │ └── server.properties │ └── server.toml └── vanilla │ ├── README.md │ ├── config │ └── server.properties │ └── server.toml ├── flake.lock ├── flake.nix ├── hotreload.schema.json ├── mkdocs.yml ├── network.schema.json ├── res ├── aikars_flags ├── cfg │ └── lp │ │ └── config.bukkit.yaml ├── default_dockerfile ├── default_dockerignore ├── default_readme ├── default_readme_network ├── markdown_links ├── proxy_flags ├── server.properties └── workflows │ ├── packwiz.yml │ └── test.yml ├── servertoml.schema.json └── src ├── app ├── actions.rs ├── caching.rs ├── downloading.rs ├── feedback.rs ├── from_string.rs ├── hashing.rs ├── mod.rs ├── progress.rs ├── resolvable.rs └── steps.rs ├── commands ├── add │ ├── mod.rs │ └── modrinth.rs ├── build.rs ├── cache.rs ├── completions.rs ├── dedupe.rs ├── dev.rs ├── download.rs ├── eject.rs ├── env │ ├── docker.rs │ ├── gitignore.rs │ ├── mod.rs │ ├── workflow_packwiz.rs │ └── workflow_test.rs ├── export │ ├── mod.rs │ ├── mrpack.rs │ └── packwiz.rs ├── import │ ├── datapack.rs │ ├── mod.rs │ ├── mrpack.rs │ ├── packwiz.rs │ └── url.rs ├── info.rs ├── init │ └── mod.rs ├── markdown.rs ├── mod.rs ├── pull.rs ├── run.rs ├── version.rs ├── world │ ├── mod.rs │ ├── pack.rs │ └── unpack.rs └── ws │ └── mod.rs ├── core ├── addons.rs ├── bootstrap.rs ├── mod.rs ├── scripts.rs ├── serverjar.rs └── worlds.rs ├── hot_reload ├── config.rs ├── mod.rs └── pattern_serde.rs ├── interop ├── hooks.rs ├── java.rs ├── markdown.rs ├── mod.rs ├── mrpack.rs ├── packwiz.rs └── worlds.rs ├── main.rs ├── model ├── app_config │ └── mod.rs ├── clientsidemod.rs ├── downloadable │ ├── markdown.rs │ ├── meta.rs │ └── mod.rs ├── hooks.rs ├── lockfile.rs ├── mod.rs ├── network.rs ├── serverlauncher.rs ├── servertoml.rs ├── servertype │ ├── interactive.rs │ ├── meta.rs │ ├── mod.rs │ └── parse.rs └── world.rs ├── sources ├── curserinth.rs ├── fabric.rs ├── forge.rs ├── github.rs ├── hangar.rs ├── jenkins.rs ├── maven.rs ├── mclogs.rs ├── mod.rs ├── modrinth.rs ├── neoforge.rs ├── papermc.rs ├── purpur.rs ├── quilt.rs ├── spigot.rs └── vanilla.rs └── util ├── env.rs ├── maven_import.rs ├── md.rs ├── mod.rs └── packwiz.rs /.dockerignore: -------------------------------------------------------------------------------- 1 | .dockerignore 2 | .git 3 | .gitignore 4 | Dockerfile 5 | target/ 6 | test/ 7 | docs/ 8 | examples/ 9 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | 2 | # mcman: use lfs for worlds 3 | **/worlds/*.zip filter=lfs diff=lfs merge=lfs -text 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | pull_request: 4 | 5 | name: Clippy and build 6 | jobs: 7 | clippy: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v3 11 | - run: rustup toolchain install stable 12 | - uses: Swatinem/rust-cache@v2 13 | - uses: actions-rs/clippy-check@v1 14 | with: 15 | token: ${{ secrets.GITHUB_TOKEN }} 16 | args: --all-features 17 | build: 18 | strategy: 19 | matrix: 20 | os: [ubuntu-latest, windows-latest] 21 | runs-on: ${{ matrix.os }} 22 | steps: 23 | - uses: actions/checkout@v3 24 | - run: rustup toolchain install stable 25 | - uses: Swatinem/rust-cache@v2 26 | - run: cargo build --release 27 | - uses: actions/upload-artifact@v4 28 | with: 29 | name: mcman-${{ matrix.os }} 30 | path: | 31 | target/release/mcman* 32 | !target/release/mcman.d 33 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | branches: [ 'main' ] 4 | 5 | env: 6 | REGISTRY: ghcr.io 7 | IMAGE_NAME: ${{ github.repository }} 8 | 9 | name: Publish docker image 10 | jobs: 11 | publish: 12 | runs-on: ubuntu-latest 13 | permissions: 14 | contents: read 15 | packages: write 16 | 17 | steps: 18 | - uses: actions/checkout@v3 19 | 20 | - uses: docker/setup-buildx-action@v2 21 | 22 | - name: Log in to the Container registry 23 | uses: docker/login-action@v2 24 | with: 25 | registry: ${{ env.REGISTRY }} 26 | username: ${{ github.actor }} 27 | password: ${{ secrets.GITHUB_TOKEN }} 28 | 29 | - name: Extract metadata (tags, labels) for Docker 30 | id: meta 31 | uses: docker/metadata-action@v4 32 | with: 33 | images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} 34 | tags: type=raw,value=latest,enable=true 35 | 36 | - name: Build and push Docker image 37 | uses: docker/build-push-action@v4 38 | with: 39 | context: . 40 | push: true 41 | tags: ${{ steps.meta.outputs.tags }} 42 | labels: ${{ steps.meta.outputs.labels }} -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: "Create Draft Release" 2 | on: 3 | workflow_dispatch: 4 | 5 | jobs: 6 | draft-release: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - name: Download artifacts 10 | id: download-artifact 11 | uses: dawidd6/action-download-artifact@v3 12 | with: 13 | workflow: build.yml 14 | workflow_conclusion: success 15 | - uses: marvinpinto/action-automatic-releases 16 | with: 17 | repo_token: ${{ secrets.GITHUB_TOKEN }} 18 | draft: true 19 | files: | 20 | mcman.exe 21 | mcman 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # rust 2 | target/ 3 | # developer testing 4 | test/ 5 | # mkdocs output 6 | site/ 7 | 8 | # mcman: Exclude mcman build outputs 9 | **/server 10 | # mcman: Exclude env secrets 11 | .env 12 | # mcman: Exclude exported mrpacks 13 | *.mrpack 14 | 15 | # mcman: Exclude local dotenv files 16 | **/.env 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /CATALOG.md: -------------------------------------------------------------------------------- 1 | # Catalog 2 | 3 | Here are some snippets for some popular minecraft projects. 4 | 5 | Note: No guarantees for this page :p 6 | 7 | ps. none of these are ours pls dont dmca us thanks 8 | 9 | ## LuckPerms 10 | 11 | ```toml 12 | [[plugins]] 13 | type = "jenkins" 14 | url = "https://ci.lucko.me" 15 | job = "LuckPerms" 16 | # artifact = "LuckPerms-Bukkit" 17 | # artifact = "LuckPerms-Forge" 18 | # artifact = "LuckPerms-Fabric" 19 | # artifact = "LuckPerms-Sponge" 20 | # artifact = "LuckPerms-Velocity" 21 | # artifact = "LuckPerms-Bungee" 22 | ``` 23 | 24 | ## FastAsyncWorldEdit 25 | 26 | Spigot 1.16.5-latest 27 | 28 | ```toml 29 | [[plugins]] 30 | type = "jenkins" 31 | url = "https://ci.athion.net" 32 | job = "FastAsyncWorldEdit" 33 | artifact = "Bukkit" 34 | ``` 35 | 36 | Spigot 1.16.5-1.18.2 (legacy) 37 | 38 | ```toml 39 | [[plugins]] 40 | type = "url" 41 | url = "https://github.com/IntellectualSites/download/raw/gh-pages/artifacts/Fawe/FastAsyncWorldEdit-Bukkit-2.2.1-SNAPSHOT-193.jar" 42 | ``` 43 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "mcman" 3 | version = "0.4.5" 4 | edition = "2021" 5 | rust-version = "1.75" 6 | repository = "https://github.com/ParadigmMC/mcman" 7 | homepage = "https://paradigmmc.github.io/mcman/" 8 | authors = ["ParadigmMC"] 9 | license = "gpl-3" 10 | description = "Powerful Minecraft Server Manager CLI" 11 | documentation = "https://paradigmmc.github.io/mcman/" 12 | keywords = [ 13 | "minecraft", "server" 14 | ] 15 | categories = ["command-line-utilities", "config"] 16 | 17 | [profile.release] 18 | strip = true 19 | lto = true 20 | opt-level = "s" 21 | 22 | [lints.clippy] 23 | all = "deny" 24 | pedantic = "warn" 25 | missing_docs_in_private_items = "allow" 26 | missing_errors_doc = "allow" 27 | module_name_repetitions = "allow" 28 | struct_excessive_bools = "allow" 29 | 30 | [dependencies] 31 | anyhow = "1.0" 32 | cached = "0.47" 33 | clap = { version = "4.4", features = ["derive"] } 34 | clap_complete = { version = "4.5", optional = true } 35 | confique = { version = "0.2", default-features = false, features = ["toml"] } 36 | console = "0.15" 37 | dialoguer = "0.11" 38 | digest = "0.10" 39 | dirs = "5.0" 40 | glob = "0.3" 41 | hex = "0.4" 42 | indexmap = "2.1" 43 | indicatif = "0.17" 44 | mcapi = { git = "https://github.com/ParadigmMC/mcapi.git" } 45 | md-5 = "0.10" 46 | notify-debouncer-full = { version = "0.3" } 47 | opener = "0.6" 48 | pathdiff = { git = "https://github.com/Manishearth/pathdiff.git" } 49 | rpackwiz = { git = "https://github.com/vgskye/rpackwiz.git" } 50 | regex = "1.10" 51 | reqwest = { version = "0.11", features = ["json", "stream", "rustls-tls"], default-features = false } 52 | roxmltree = "0.19" 53 | semver = "1.0" 54 | serde = { version = "1.0", features = ["derive"] } 55 | serde_json = "1.0" 56 | sha1 = "0.10" 57 | sha2 = "0.10" 58 | tempfile = "3.9" 59 | tokio = { version = "1.35", features = ["macros", "rt", "rt-multi-thread", "process", "signal", "io-std"] } 60 | tokio-stream = "0.1" 61 | tokio-util = { version = "0.7", features = ["io"] } 62 | toml = "0.8" 63 | walkdir = "2.4" 64 | zip = "0.6" 65 | 66 | [features] 67 | autocomplete = ["clap_complete"] 68 | default = ["autocomplete"] -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM rust:alpine as builder 2 | WORKDIR /app 3 | RUN rustup target add x86_64-unknown-linux-musl 4 | RUN apk add --no-cache musl-dev 5 | 6 | COPY . . 7 | RUN --mount=type=cache,target=/app/target \ 8 | --mount=type=cache,target=/usr/local/cargo/git \ 9 | --mount=type=cache,target=/usr/local/cargo/registry \ 10 | cargo build --target x86_64-unknown-linux-musl --release && \ 11 | cp /app/target/x86_64-unknown-linux-musl/release/mcman /app/mcman 12 | 13 | FROM alpine 14 | RUN apk add --no-cache ca-certificates 15 | COPY --from=builder /app/mcman /usr/bin/mcman 16 | -------------------------------------------------------------------------------- /docs/commands/build.md: -------------------------------------------------------------------------------- 1 | # `mcman build` 2 | 3 | See the page for [Building](../concepts/building.md) for more info 4 | 5 | ## `--force` 6 | 7 | The `--force` flag can be used to make mcman not skip already downloaded files, basically acting like the output directory is empty. 8 | 9 | ## `--output ` 10 | 11 | You can alternatively set the output folder manually using `--output ` option. The default is `server`. 12 | 13 | ## `--skip`/`-s` 14 | 15 | You can use the `--skip`/`-s` flag to skip stages. 16 | 17 | - Use the flag multiple times to skip many: `-s bootstrap -s worlds` 18 | - The stages are: `plugins`, `mods`, `worlds` and `bootstrap` 19 | 20 | ## After building 21 | 22 | After building, you can start the server with the launch scripts if theyre not [disabled](../reference/server-launcher.md): 23 | 24 | === "Windows" 25 | ```bat 26 | cd server 27 | call start.bat 28 | ``` 29 | 30 | 31 | === "Linux" 32 | ```sh 33 | cd server 34 | ./start.sh 35 | ``` 36 | -------------------------------------------------------------------------------- /docs/commands/cache.md: -------------------------------------------------------------------------------- 1 | # `mcman cache` 2 | 3 | Provides some utilities for managing your local cache directory. 4 | 5 | ## `mcman cache path` 6 | 7 | Prints the cache path. This is generally: 8 | 9 | - Windows: `%LocalAppData%/mcman` 10 | - Linux: `~/.cache/mcman` 11 | 12 | ## `mcman cache open` 13 | 14 | Opens the cache folder using a file explorer 15 | 16 | ## `mcman cache list` 17 | 18 | Lists the cache entries. 19 | 20 | ``` 21 | $ mcman cache list 22 | Listing cache... 23 | Folder: C:\Users\dennis\AppData\Local\mcman 24 | => github - 24 entries 25 | => modrinth - 278 entries 26 | => papermc - 1 entries 27 | 303 entries in 3 namespaces in total 28 | ``` 29 | 30 | ??? note "Detailed listing" 31 | You can use the `-d` flag to get a more detailed list 32 | 33 | ``` 34 | $ mcman cache list -d 35 | Listing cache... 36 | Folder: C:\Users\dennis\AppData\Local\mcman 37 | => github - 8 entries 38 | └ emilyploszaj 39 | └ EngineHub 40 | └ LemmaEOF 41 | └ MerchantPug 42 | └ ModFest 43 | └ NucleoidMC 44 | └ Patbox 45 | └ TheEpicBlock 46 | => modrinth - 10 entries 47 | └ 10DZYVis 48 | └ 13RpG7dA 49 | └ 14Z3YVAP 50 | └ 1cjUVbYD 51 | └ 1eAoo2KR 52 | └ 1IjD5062 53 | └ 1itdse3V 54 | └ 1LrBk5C6 55 | └ 1qeWG44Y 56 | └ 2qcCxsBR 57 | => papermc - 1 entries 58 | └ paper 59 | 19 entries in 3 namespaces in total 60 | ``` 61 | 62 | ## `mcman cache clear` 63 | 64 | 65 | -------------------------------------------------------------------------------- /docs/commands/dev.md: -------------------------------------------------------------------------------- 1 | # `mcman dev` 2 | 3 | Starts a [development session](../concepts/dev.md) 4 | -------------------------------------------------------------------------------- /docs/commands/env.md: -------------------------------------------------------------------------------- 1 | # `mcman env ` 2 | 3 | Configure environment things. 4 | 5 | ## `mcman env gitignore` 6 | 7 | Adds some ignore items in your repository's `.gitignore` 8 | 9 | `git` should be installed and you should be in a repository for this to work 10 | 11 | ## `mcman env docker` 12 | 13 | Write the default Dockerfile and .dockerignore 14 | -------------------------------------------------------------------------------- /docs/commands/export.md: -------------------------------------------------------------------------------- 1 | # `mcman export ...` 2 | 3 | Exporting commands 4 | 5 | ## `mcman export mrpack [filename]` 6 | 7 | Export the server as an `mrpack` (modrinth modpack) file 8 | 9 | If `[filename]` argument isn't given, it will be exported as `${SERVER_NAME}.mrpack` 10 | 11 | See also: [special variables](#special-variables) that contain export-related variables 12 | 13 | ## `mcman export packwiz` 14 | 15 | > **Alias & Full Command:** `mcman export pw [-o --output ] [--cfcdn]` 16 | 17 | Export the server as a packwiz pack, by default to `pack/` folder. 18 | 19 | If you are in a git repo, mcman will give you the githack url to the generated `pack.toml` at the end of the export. 20 | 21 | ??? "Extra options (output & cfcdn)" 22 | You can use the `--output ` option to set a custom destination to the pack. 23 | 24 | Using `mcman export pw --output packwiz/pack` will create `pack.toml` to `./packwiz/pack/pack.toml` 25 | 26 | If the `--cfcdn` flag is used, every `curserinth` downloadable will use `download.mode = "url"` with `download.url` being the url from curseforge's cdn. 27 | 28 | If its not used, `download.mode = "metadata:curseforge"` is used with `update.curseforge = { .. }` (default packwiz behavior) 29 | 30 | See also: [special variables](#special-variables) that contain export-related variables 31 | -------------------------------------------------------------------------------- /docs/commands/import.md: -------------------------------------------------------------------------------- 1 | # `mcman import ...` 2 | 3 | > Alias: `mcman i ...` 4 | 5 | Commands related to importing 6 | 7 | ## `mcman import url ` 8 | 9 | Imports a plugin or a mod from a url. 10 | 11 | Supports: 12 | 13 | - `[cdn.]modrinth.com` 14 | - `curserinth.kuylar.dev` 15 | - `www.curseforge.com` 16 | - `www.spigotmc.org` 17 | - `github.com` 18 | - If not any of those, will prompt with **direct url** or **jenkins** 19 | 20 | Example usages: 21 | 22 | ```sh 23 | mcman import url https://modrinth.com/plugin/imageframe 24 | mcman import url https://www.spigotmc.org/resources/armorstandeditor-reborn.94503/ 25 | mcman import url https://ci.athion.net/job/FastAsyncWorldEdit/ 26 | ``` 27 | 28 | ## `mcman import datapack ` 29 | 30 | Like [import url](#mcman-import-url-url), but imports as a datapack rather than a plugin or a mod. 31 | 32 | Example usage: 33 | 34 | ```sh 35 | # datapack alias is dp 36 | mcman import dp https://modrinth.com/plugin/tectonic 37 | ``` 38 | 39 | ## `mcman import mrpack ` 40 | 41 | Imports a [mrpack](https://docs.modrinth.com/docs/modpacks/format_definition/) file (modrinth modpacks) 42 | 43 | **Note:** [`mcman init`](#mcman-init) supports mrpacks 44 | 45 | The source can be: 46 | 47 | - A direct URL to a `.mrpack` file 48 | - A local file path 49 | - Modpack URL (`https://modrinth.com/modpack/{id}`) 50 | - Modrinth project id prefixed with `mr:` 51 | 52 | Example usages: 53 | 54 | ```sh 55 | # direct link 56 | mcman import mrpack https://cdn.modrinth.com/data/xldzprsQ/versions/xWFqQBjM/Create-Extra-full-1.1.0.mrpack 57 | # only /modpack urls 58 | mcman import mrpack https://modrinth.com/modpack/create-extra 59 | # prefixed 60 | mcman import mrpack mr:simply-skyblock 61 | # local file 62 | mcman import mrpack My-Pack.mrpack 63 | ``` 64 | 65 | ## `mcman import packwiz ` 66 | 67 | > Alias: `mcman i pw ` 68 | 69 | Imports a [packwiz](https://packwiz.infra.link/) pack 70 | 71 | !!! note 72 | [`mcman init`](./init.md) supports initializing with packwiz 73 | 74 | The source can be: 75 | 76 | - A packwiz pack URL 77 | - A local file path to `pack.toml` 78 | 79 | Example usages: 80 | 81 | ```sh 82 | mcman import packwiz https://raw.githack.com/ParadigmMC/mcman-example-quilt/main/pack/pack.toml 83 | mcman import packwiz ../pack.toml 84 | ``` 85 | -------------------------------------------------------------------------------- /docs/commands/index.md: -------------------------------------------------------------------------------- 1 | # CLI Commands 2 | 3 | This section shows the commands of mcman. You can type `mcman`, `mcman help` or `mcman --help` for a basic list of it. 4 | 5 | ## Cheatsheet 6 | 7 | - Create 8 | - `mcman init`: create new server 9 | - `mcman init --mrpack `: create server from mrpack 10 | - `mcman init --packwiz `: create server from packwiz 11 | - Build 12 | - `mcman build`: build the server 13 | - `mcman run`: build then run the server 14 | - `mcman run --test`: build then run to test if it works 15 | - `mcman dev`: start a dev session 16 | - Addons 17 | - `mcman import url `: import an addon from url 18 | - `mcman import datapack `: import datapacks 19 | - Export/Import 20 | - `mcman import packwiz `: import packwiz packs 21 | - `mcman import mrpack `: import mrpacks 22 | - `mcman export packwiz [pack folder]`: export as packwiz pack 23 | - `mcman export mrpack [filename.mrpack]`: export as mrpack 24 | - Info 25 | - `mcman info`: show info about the server 26 | - `mcman version`: show version 27 | - Cache 28 | - `mcman cache path`: print cache path 29 | - `mcman cache open`: open the cache folder 30 | - `mcman cache list [-d]`: list caches, `-d` for detailed 31 | - `mcman cache clear`: delete caches without confirm 32 | - Misc 33 | - `mcman markdown`: render markdown templates 34 | - `mcman download
`: download a downloadable 35 | - `mcman world unpack [world]`: unzip a world 36 | - `mcman pull `: pull files from `server/` to `config/` 37 | - `mcman env gitignore`: edit git dotfiles 38 | - `mcman env docker`: create default docker files 39 | -------------------------------------------------------------------------------- /docs/commands/info.md: -------------------------------------------------------------------------------- 1 | # `mcman info` 2 | 3 | Shows info about the server in the terminal. 4 | -------------------------------------------------------------------------------- /docs/commands/init.md: -------------------------------------------------------------------------------- 1 | # `mcman init` 2 | 3 | Initializes a new server in the current directory. 4 | 5 | This command is interactive. Just run `mcman init`! 6 | 7 | See the [getting started](../concepts/getting-started.md) tutorial for what to do next 8 | 9 | > **Full Command:** `mcman init [--name ] [--mrpack | --packwiz ]` 10 | 11 | ??? "📦 Importing from a mrpack (modrinth modpack)" 12 | You can use the `--mrpack` flag on `mcman init` to import from an mrpack while initializing a server. 13 | 14 | - If its from modrinth, like [adrenaserver](https://modrinth.com/modpack/adrenaserver): `mcman init --mrpack mr:adrenaserver` 15 | 16 | Use `mr:` and then the project id/slug of the modpack (should be visible on the url) 17 | 18 | - You can also just paste in the modpack page's url: `mcman init --mrpack https://modrinth.com/modpack/adrenaserver` 19 | 20 | - If its from another source, you can provide a download link to it: `mcman init --mrpack https://example.com/pack.mrpack` 21 | 22 | - If its a file: `mcman init --mrpack ../modpacks/pack.mrpack` 23 | 24 | If your server is already initialized, use the `mcman import mrpack ` command. The source argument also accepts the sources defined above. 25 | 26 | Example using [Adrenaserver](https://modrinth.com/modpack/adrenaserver): 27 | 28 | ```sh 29 | # these are all identical 30 | mcman init --mrpack mr:adrenaserver 31 | mcman init --mrpack https://modrinth.com/modpack/adrenaserver 32 | mcman init --mrpack https://cdn.modrinth.com/data/H9OFWiay/versions/2WXUgVhc/Adrenaserver-1.4.0%2B1.20.1.quilt.mrpack 33 | ``` 34 | 35 | ??? "📦 Importing from a packwiz pack" 36 | You can use the `--packwiz` (alias `--pw`) flag on `mcman init` to import a packwiz pack while initializing. 37 | 38 | **If the pack is in your filesystem**: 39 | 40 | ```sh 41 | mcman init --pw path/to/pack.toml 42 | ``` 43 | 44 | **If the pack is online**: 45 | 46 | ```sh 47 | mcman init --pw https://raw.githack.com/EXAMPLE/EXAMPLE/main/pack.toml 48 | ``` 49 | 50 | If your server is already initialized, use the `mcman import packwiz ` command. The source argument also accepts the sources defined above. 51 | 52 | ??? question "I dont see a Dockerfile/.gitignore" 53 | If mcman can't detect a git repository, it wont write a `.gitignore` 54 | 55 | The same applies for `Dockerfile` when `docker --version` fails. 56 | 57 | You can use [`mcman env`](./env.md) to get those files. 58 | -------------------------------------------------------------------------------- /docs/commands/markdown.md: -------------------------------------------------------------------------------- 1 | # `mcman markdown` 2 | 3 | > Alias: `mcman md` 4 | 5 | This command refreshes the markdown files defined in the [server.toml](../reference/markdown-options) files with the templates. 6 | 7 | See [markdown options](../reference/markdown-options) for more information. 8 | -------------------------------------------------------------------------------- /docs/commands/pull.md: -------------------------------------------------------------------------------- 1 | # `mcman pull ` 2 | 3 | 'Pulls' a file from `server/` to `config/` 4 | 5 | Example usage: 6 | 7 | ```sh 8 | ~/smp $ ls 9 | ... 10 | server.toml 11 | ... 12 | 13 | ~/smp $ cd server/config/SomeMod 14 | 15 | ~/smp/server/config/SomeMod $ mcman pull config.txt 16 | server/config/SomeMod/config.txt => config/config/SomeMod/config.txt 17 | ``` 18 | -------------------------------------------------------------------------------- /docs/commands/run.md: -------------------------------------------------------------------------------- 1 | # `mcman run` 2 | 3 | See [here](../concepts/building.md) for more info 4 | 5 | Supports the same arguments as [mcman build](./build.md) 6 | 7 | Builds the server and runs it. This is kind of the same as running `mcman build && cd server && start` 8 | 9 | ## `mcman run --test` 10 | 11 | You can use the `--test` option to test if your server works. mcman will build and run the server and see if it fully starts up. If it crashes, stops, or doesnt succeed, mcman will report the issue and exit with code `1`. 12 | 13 | If `options.upload_to_mclogs` is `true` in `server.toml`, mcman will upload `latest.log` and the crash log (if it crashed) to [mclo.gs](https://mclo.gs/) and print the URL to the console. 14 | 15 | You can use CI/CD to test if your server works. For example, [this](https://github.com/ParadigmMC/mcman-bc23/blob/1938a567a2324607d816f17481e49c922af1ed87/.github/workflows/bc23test.yml) is a github workflow that tests if the BlanketCon 23 server boots up successfully. 16 | 17 | mcman's criteria for a "successful boot" is this line: 18 | 19 | ``` 20 | [12:57:24] [Server thread/INFO]: Done (5.290s)! For help, type "help" 21 | ``` 22 | -------------------------------------------------------------------------------- /docs/commands/version.md: -------------------------------------------------------------------------------- 1 | # `mcman version` 2 | 3 | Shows the version of **mcman** and checks if its up to date. 4 | -------------------------------------------------------------------------------- /docs/commands/world.md: -------------------------------------------------------------------------------- 1 | # `mcman world pack|unpack` 2 | 3 | ## `mcman world unpack ` 4 | 5 | Unpack 6 | -------------------------------------------------------------------------------- /docs/concepts/building.md: -------------------------------------------------------------------------------- 1 | # Building 2 | 3 | Normally, in your ordinary Minecraft server, **everything** (including your server jar, plugins, config files yada yada) is in the **"main"** directory. 4 | 5 | `mcman` does things differiently - that folder with bunch of jar files, configs and worlds are all under the **`server/`** folder. 6 | 7 | Why? This makes sure your files are fully local and not uploaded to git, and makes the file structure much cleaner. You want to fully wipe the server data? Just delete `server/`! 8 | 9 | ## What is 'Building'? 10 | 11 | If you just created a `server.toml` file or are looking in a git repository, you wont see any `server/` folder. 12 | 13 | **Building** refers to `mcman` doing all the work for you, i.e. downloading jars, to give you your own local `server/` folder with a whole Minecraft server ready to run! 14 | 15 | ## How do I build the server? 16 | 17 | You use the **`mcman build`** command. 18 | 19 | ## I'm tired of calling 'start.sh' 20 | 21 | There is a convenient command for you then! You can use `mcman run` to *build and run* your server. 22 | 23 | ## Is there a way I can develop my server easier? 24 | 25 | Yes! We have a command for that, [read here](./dev.md)! 26 | 27 | ## Well okay, how does it work? 28 | 29 | 1. Server jar is downloaded (or installed) 30 | - Some server types (such as Spigot or Forge) dont have jar files mcman can just download, so it needs to **install** them by running their installers. 31 | 2. Addons (plugins and mods) are downloaded 32 | 3. Worlds are [unpacked or downloaded](./using-worlds.md) if they dont exist 33 | 4. Datapacks are downloaded 34 | 5. Files get [bootstrapped](./variables.md) with variables (`config/` -> `server/`) 35 | 6. [Launch scripts](../reference/server-launcher.md) are created 36 | 37 | -------------------------------------------------------------------------------- /docs/concepts/caching.md: -------------------------------------------------------------------------------- 1 | # Caching 2 | 3 | mcman will try to cache downloaded files and other metadata such as Github API requests. 4 | 5 | The cache folder will generally be: 6 | 7 | - Windows: `%LocalAppData%/mcman` 8 | - Linux: `~/.cache/mcman` 9 | 10 | You can also manage the cache using the `mcman cache` command. 11 | 12 | - `mcman cache path`: Prints the cache path 13 | - `mcman cache list`: Lists caches 14 | - `-d` for detailed 15 | - `mcman cache open`: Opens the cache folder using a file explorer 16 | - `mcman cache clear`: Clears the cache without confirmation 17 | 18 | ## Folders 19 | 20 | Most sources have their own folders: 21 | 22 | - Modrinth: `modrinth/{project}/{version}/{file}` 23 | - Curserinth: `curserinth/{project}/{version}/{file}` 24 | - Github: 25 | - Metadata: `github/{owner}/{repo}/releases.json` 26 | - Releases: `github/{owner}/{repo}/releases/{tag}/{file}` 27 | - Hangar: `hangar/{owner}/{proj}/{version}/{file}` 28 | - Jenkins: `jenkins/{url}/{...job}/{build}/{file}` 29 | - Maven: `maven/{url}/{...group}/{artifact}/{version}/{file}` 30 | - PaperMC: `papermc/{proj}/{proj}-{mcver}-{build}.jar` 31 | -------------------------------------------------------------------------------- /docs/concepts/dev.md: -------------------------------------------------------------------------------- 1 | # Development Mode 2 | 3 | `mcman dev` is a command that allows you to configure your server more efficiently. 4 | 5 | ## What does it do? 6 | 7 | Development Mode does a few things - mostly watching files for changes. 8 | 9 | It watches: 10 | 11 | - `server.toml`, and rebuilds the server when you change it 12 | - the `config/**` directory 13 | - and `hotreload.toml` 14 | 15 | ## Actions for when files change 16 | 17 | The `hotreload.toml` file allows you to create **actions** to execute when some files under `config/` change after they have been automatically [bootstrapped](./variables.md). 18 | 19 | ``` toml title="hotreload.toml" 20 | [[files]] 21 | path = "server.properties" 22 | action = "reload" 23 | 24 | [[files]] 25 | path = "plugins/Something/**" 26 | action = "/something reload" 27 | ``` 28 | 29 | For every file entry, define `path` as a glob pattern to match files. 30 | 31 | And for `action`, you can use 32 | 33 | - `"reload"` to reload the server 34 | - `"restart"` to rebuild the server 35 | - and any value starting with `/` to send commands, for example: `"/say hello"` 36 | 37 | -------------------------------------------------------------------------------- /docs/concepts/importing-modpacks.md: -------------------------------------------------------------------------------- 1 | # Importing Modpacks 2 | 3 | `mcman` can import from the [mrpack](https://modrinth.com/modpacks) format (modrinth modpacks) or [packwiz](https://packwiz.infra.link/) packs. 4 | 5 | ??? question "Can I import after initializing?" 6 | Yes you can! 7 | 8 | - For mrpacks: `mcman import mrpack ` 9 | - For packwiz packs: `mcman import packwiz ` 10 | 11 | The source arguments are the same 12 | 13 | ## mrpack 14 | 15 | You can import mrpacks with the `--mrpack` flag while initializing: 16 | 17 | ``` 18 | mcman init --mrpack 19 | ``` 20 | 21 | The `source` argument can be 22 | 23 | * An URL to a direct download of the `.mrpack` file 24 | * A local path to the `.mrpack` file 25 | * A modpack from [Modrinth](https://modrinth.com/modpacks) prefixed with "`mr:`" 26 | * For example, the modpack [Packed](https://modrinth.com/modpack/packed) would be written as "`mr:packed`" 27 | 28 | ## packwiz 29 | 30 | Like mrpacks, you can import while initializing with: 31 | 32 | ``` 33 | mcman init --packwiz 34 | ``` 35 | 36 | The `source` argument can be 37 | 38 | * An URL with `http`/`https` scheme 39 | * Path to a local `pack.toml` file 40 | 41 | ## Whats next? 42 | 43 | Tutorial -> Getting Started -> [Building](./getting-started.md#building) 44 | -------------------------------------------------------------------------------- /docs/concepts/network.md: -------------------------------------------------------------------------------- 1 | # Networks 2 | 3 | If you want to manage multiple servers at once like a network, you can use `network.toml`: 4 | 5 | ```toml 6 | name = "CoolNetwork" 7 | port = 25565 8 | ``` 9 | 10 | ## Servers 11 | 12 | In `network.toml`, define servers in the `servers` table: 13 | 14 | ```toml 15 | name = "CoolNetwork" 16 | port = 25565 17 | 18 | [servers.lobby] 19 | port = 25566 20 | 21 | [servers.game1] 22 | port = 25567 23 | ``` 24 | 25 | In the folder structure, keep the servers under the `servers/` folder: 26 | 27 | ```yaml 28 | cool_network 29 | ├─ network.toml 30 | └─ servers/ 31 | ├─ lobby 32 | │ └─ server.toml 33 | └─ game1 34 | └─ server.toml 35 | ``` 36 | 37 | If needed, you can optionally define `ip_address` in servers. This is `"127.0.0.1"` by default. 38 | 39 | At the moment, most of these rules aren't used or enforced, but kept in here so other tools could be created around this. 40 | 41 | ## Variables 42 | 43 | Just like the normal `server.toml` variables, you can define custom variables in `network.toml`: 44 | 45 | ```toml 46 | [variables] 47 | SOME = "thing" 48 | ``` 49 | 50 | Network variables need to be prefixed with `NW_` while accessing them. 51 | 52 | So instead of using `${SOME}` to access it, `${NW_SOME}` can be used. 53 | 54 | ## Special Variables 55 | 56 | Here are some more special variables. 57 | 58 | !!! note 59 | You can use the `PORT_name` and `IP_name` environment variables to override server ip addresses and ports. The `name` must be the name of the server as defined in `server.toml`. 60 | 61 | - `SERVER_IP`: the IP address of the server 62 | - `SERVER_PORT`: the port of the server 63 | - `NETWORK_NAME`: name of the network 64 | - `NETWORK_PORT`: the defined port 65 | - `NETWORK_SERVERS_COUNT`: amount of servers defined 66 | 67 | You can also get the port or IP of another server via 68 | 69 | - `NW_SERVER_name_IP` 70 | - `NW_SERVER_name_PORT` 71 | - `NW_SERVER_name_ADDRESS`: Basically "ip:port" 72 | 73 | These generate a table of servers that can be used in proxy server configurations: 74 | 75 | - `NETWORK_VELOCITY_SERVERS` for Velocity 76 | - `NETWORK_BUNGEECORD_SERVERS` for BungeeCord/Waterfall 77 | 78 | === "Usage in Velocity" 79 | ```toml 80 | #${NETWORK_VELOCITY_SERVERS} 81 | ``` 82 | 83 | === "Usage in BungeeCord" 84 | ```toml 85 | #${NETWORK_BUNGEECORD_SERVERS} 86 | ``` 87 | 88 | You can comment the line because it will start with a comment/disclaimer. 89 | -------------------------------------------------------------------------------- /docs/concepts/options.md: -------------------------------------------------------------------------------- 1 | # Options/Misc 2 | 3 | Here are some misc. options of mcman 4 | 5 | ## Setting the Java binary 6 | 7 | If you want `mcman` to use a custom java binary, you can set the environment variable `JAVA_BIN` to its path. 8 | 9 | For servers that have the `launcher.java_version` field set, mcman will try to use the `JAVA_*_BIN` environment first before `JAVA_BIN`. 10 | 11 | For example, for 12 | 13 | ```toml 14 | [launcher] 15 | java_version = "16" 16 | ``` 17 | 18 | `mcman` will first check for `JAVA_16_BIN`, then `JAVA_BIN` and if both aren't set, `"java"` will be used as default. 19 | 20 | ## Disabling lockfiles 21 | 22 | To disable [Lockfile](../reference/lockfile.md)s, you can set the `MCMAN_DISABLE_LOCKFILE` environment variable to `true`. 23 | 24 | ## Addon download attempts 25 | 26 | By default, addons get 3 tries to be downloaded. To change this, set the `MAX_TRIES` environment variable to the max amount of tries. For example, set it to `1` if you want `mcman` to try only once. 27 | 28 | ## Overriding server ports in networks 29 | 30 | See the note on [this section](./network.md#special-variables) 31 | -------------------------------------------------------------------------------- /docs/concepts/using-worlds.md: -------------------------------------------------------------------------------- 1 | # Using Worlds 2 | 3 | mcman can help you manage your server's worlds. You can make mcman [download](#downloading-worlds) a world or keep the world under the [`worlds/`](#the-worlds-folder) directory. 4 | 5 | Worlds are defined in [`server.toml`](../reference/server.toml.md) under the `worlds` table. The key of the table is the world name. For example, the world with name 'city' would be `worlds.city`. 6 | 7 | ## Downloading Worlds 8 | 9 | If you set the `download` field to a [Downloadable](../reference/downloadable/index.md) mcman will download the world zip file and unzip it if the world does not exist in the output directory. 10 | 11 | ```toml 12 | [worlds.earth.download] 13 | type = "url" 14 | url = "https://example.com/cdn/worlds/earth.zip" 15 | ``` 16 | 17 | ## The `worlds/` Folder 18 | 19 | Optionally, you can store your worlds under the `worlds/` folder. 20 | 21 | ```yaml 22 | worlds 23 | ├─ lobby.zip 24 | └─ arena.zip 25 | ``` 26 | 27 | When building, if the world does not exist in the output directory, mcman will unzip the world file located in the `worlds/` folder. 28 | 29 | !!! note 30 | For your world to be unpacked, there needs to be a world entry in `server.toml` for it: 31 | 32 | ```toml 33 | [worlds.city] 34 | # just the entry is enough 35 | ``` 36 | 37 | You can also manually unpack a world using the [`mcman unpack `](../commands/world.md) command. 38 | -------------------------------------------------------------------------------- /docs/concepts/variables.md: -------------------------------------------------------------------------------- 1 | # Variables and Bootstrapping 2 | 3 | In `server.toml`, you can define variables like so: 4 | 5 | ```toml 6 | [variables] 7 | hello = "world" 8 | # define variables here 9 | ``` 10 | 11 | When you build your server, any config file with common extensions (`yml`, `json`, `toml`, `txt` etc.) will be **bootstrapped** - their contents will be replaced with variables using the variable syntax. 12 | 13 | Bootstrapping is essentially copying the file and doing a complex find-and-replace. 14 | 15 | The syntax for variables are `${name}` where `name` is the name of the variable. A colon can be used to set a default value: `${MOTD:Hi, im a Minecraft Server!}` 16 | 17 | ??? "Using environment variables" 18 | If your variables are sensitive (such as discord bot tokens) you can use environment variables: 19 | 20 | === "Linux" 21 | ```bash 22 | export TOKEN=asdf 23 | ``` 24 | 25 | === "Windows" 26 | ```bat 27 | set TOKEN=asdf 28 | ``` 29 | 30 | Environment variables can be accessed just like other variables. 31 | 32 | ## Examples 33 | 34 | Environment variable: `TOKEN=asdf` 35 | 36 | `server.toml` 37 | 38 | ```toml 39 | name = "funnies" 40 | 41 | [variables] 42 | PORT = "25500" 43 | MOTD = "welcome to funnies" 44 | WEBSITE = "https://example.com/" 45 | Prefix = "[funnies]" 46 | ``` 47 | 48 | `server.properties` 49 | 50 | === "📜 config/server.properties" 51 | ```properties 52 | server-port=${PORT:25565} 53 | gamemode=creative 54 | motd=${MOTD} 55 | online-mode=false 56 | ``` 57 | 58 | === "➡️ server/server.properties" 59 | ```properties 60 | server-port=25500 61 | gamemode=creative 62 | motd=welcome to funnies 63 | online-mode=false 64 | ``` 65 | 66 | `plugins/someplugin/config.yml` 67 | 68 | === "📜 config/..." 69 | ```yaml 70 | bossbar: "${SERVER_NAME} - ${WEBSITE}" 71 | messages: 72 | no_permissions: ${Prefix} You do not have the permissions. 73 | 74 | token: ${TOKEN} 75 | ``` 76 | 77 | === "➡️ server/..." 78 | ```yaml 79 | bossbar: "funnies - https://example.com/" 80 | messages: 81 | no_permissions: [funnies] You do not have the permissions. 82 | 83 | token: asdf 84 | ``` 85 | 86 | ## Network Variables 87 | 88 | See [Networks/Variables](./network.md#variables) for more info. 89 | 90 | ## Special Variables 91 | 92 | There are some special variables: 93 | 94 | - `SERVER_NAME`: `name` property from `server.toml` 95 | - `SERVER_VERSION`: `mc_version` property from `server.toml` 96 | - `SERVER_PORT` and `SERVER_IP`: See [Networks/Variables](./network.md) 97 | - `PLUGIN_COUNT`/`MOD_COUNT`: the number of plugins or mods 98 | - `WORLD_COUNT`: the number of defined worlds 99 | - `CLIENTSIDE_MOD_COUNT`: the number of client-side mods 100 | 101 | There are also some special variables for [network](./network.md)s which can be found [here](./network.md#special-variables) 102 | 103 | When exporting to mrpack or packwiz, these variables from `server.toml` are used: 104 | 105 | | Variable Name | mrpack - `modrinth.index.json` | packwiz - `pack.toml` | 106 | | :---------------- | :----------------------------- | :-------------------- | 107 | | `MODPACK_NAME` | `name` | `name` | 108 | | `MODPACK_SUMMARY` | `summary` | `description` | 109 | | `MODPACK_AUTHORS` | *nothing* | `author` | 110 | | `MODPACK_VERSION` | *nothing* | `version` | 111 | 112 | :3 113 | -------------------------------------------------------------------------------- /docs/diagrams/build.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ParadigmMC/mcman/00668083634c6d20b96dbc7c5f5dd21615ed244c/docs/diagrams/build.png -------------------------------------------------------------------------------- /docs/diagrams/diag1.drawio: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /docs/diagrams/diag1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ParadigmMC/mcman/00668083634c6d20b96dbc7c5f5dd21615ed244c/docs/diagrams/diag1.png -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | # mcman 2 | 3 | Welcome to the `mcman` docs! Here, have some tea and make yourself comfortable. 🫖 4 | 5 | ![mcman banner](./mcman-3.png) 6 | 7 | ## Quick Start 8 | 9 | - [Installation](./installation.md) 10 | - [Getting Started](./concepts/getting-started.md) 11 | - [Building, Running and Developing](./concepts/building.md) 12 | - [Variables and Bootstrapping](./concepts/variables.md) 13 | - [Commands](./commands) 14 | - [Reference](./reference/server.toml.md) 15 | 16 | ## About 17 | 18 | `mcman` allows you to: 19 | 20 | - Have a single `server.toml` for auto-updating plugins/mods, the server jar, worlds, etc... 21 | - get rid of the hassle of downloading and dragging over jar files, 22 | - efficiently write config files using config and environment variables, 23 | - use `git` for your servers to be able to version control and collaborate, 24 | - render markdown about the server 25 | - test if the server works using CI 26 | - and more! 27 | 28 | `mcman` is not great for: 29 | 30 | - Creating clientside modpacks or modpacks to only publish them (use [packwiz](https://packwiz.infra.link/) for that) 31 | 32 | -------------------------------------------------------------------------------- /docs/installation.md: -------------------------------------------------------------------------------- 1 | [latest-win]: https://github.com/ParadigmMC/mcman/releases/latest/download/mcman.exe 2 | [latest-linux]: https://github.com/ParadigmMC/mcman/releases/latest/download/mcman 3 | 4 | # Installation 5 | 6 | Here are a few ways to install mcman. [Whats Next? ->](./concepts/getting-started.md) 7 | 8 | === "Github Releases" 9 | You can use the links below to get the mcman executable. 10 | 11 | [:fontawesome-brands-windows: Windows][latest-win]{ .md-button } [:fontawesome-brands-linux: OSX/Linux][latest-linux]{ .md-button } 12 | 13 | [:simple-github: Github Releases Page](https://github.com/ParadigmMC/mcman/releases){ .md-button } [:simple-github: Build Action (nightly)](https://github.com/ParadigmMC/mcman/actions/workflows/build.yml){ .md-button } 14 | 15 | === "Windows: Scoop" 16 | Add the [minecraft](https://github.com/The-Simples/scoop-minecraft) bucket and install mcman: 17 | 18 | ```powershell 19 | scoop bucket add minecraft https://github.com/The-Simples/scoop-minecraft 20 | scoop install mcman 21 | ``` 22 | 23 | [Scoop](https://scoop.sh/) is a command-line installer for Windows. You can use 2 commands in powershell to install it. (4 commands in total to install mcman!) 24 | 25 | === "AUR: mcman-bin" 26 | `mcman` is [available](https://aur.archlinux.org/packages/mcman-bin) in AUR as `mcman-bin` 27 | 28 | ```sh 29 | https://aur.archlinux.org/mcman-bin.git 30 | ``` 31 | 32 | === "Linux: curl" 33 | Install to `/usr/bin` using `curl`: 34 | 35 | ```sh 36 | sudo curl -L -o /usr/bin/mcman https://github.com/ParadigmMC/mcman/releases/latest/download/mcman 37 | sudo chmod +x /usr/bin/mcman 38 | ``` 39 | 40 | === "Cargo/Rust" 41 | If you have rust installed, you can compile mcman from source: 42 | 43 | ```sh 44 | cargo install --git https://github.com/ParadigmMC/mcman.git 45 | ``` 46 | -------------------------------------------------------------------------------- /docs/mcman-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ParadigmMC/mcman/00668083634c6d20b96dbc7c5f5dd21615ed244c/docs/mcman-3.png -------------------------------------------------------------------------------- /docs/reference/clientsidemod.md: -------------------------------------------------------------------------------- 1 | # ClientSideMod 2 | 3 | This is basically a [Downloadable](./downloadable) of any type with some extra fields: 4 | 5 | | Name | Type | Description | 6 | | --- | --- | --- | 7 | | `optional` | bool | Marks if optional or not | 8 | | `desc` | string | Provide a description | 9 | 10 | These fields are used for exporting to [mrpack]() or [packwiz]() 11 | 12 | !!! example 13 | ```toml title="server.toml" 14 | [[clientsidemods]] 15 | type = "modrinth" 16 | id = "3dskinlayers" 17 | version = "JHapWF9O" 18 | optional = true 19 | desc = "It adds 3D skin layers :moyai:" 20 | ``` 21 | -------------------------------------------------------------------------------- /docs/reference/downloadable/curserinth.md: -------------------------------------------------------------------------------- 1 | # CurseRinth 2 | 3 | Downloads a mod from [CurseRinth](https://curserinth.kuylar.dev/)'s API, which is basically [curseforge](https://www.curseforge.com/) 4 | 5 | !!! example 6 | ```toml title="Downloads JustEnoughItems from Curseforge" 7 | type = "curserinth" 8 | id = "jei" 9 | version = "4593548" 10 | ``` 11 | 12 | **Fields:** 13 | 14 | | Name | Type | Description | 15 | | --------- | --------------------- | ----------------------------- | 16 | | `type` | `"curserinth"`/`"cr"` | | 17 | | `id` | string | The slug or the id of the mod | 18 | | `version` | string/`"latest"` | The file id of the mod | 19 | -------------------------------------------------------------------------------- /docs/reference/downloadable/custom-url.md: -------------------------------------------------------------------------------- 1 | # Custom URL 2 | 3 | Download from a direct download link. 4 | 5 | !!! example 6 | ```toml 7 | type = "url" 8 | url = "https://example.com/download/Example.jar" 9 | filename = "example-mod.jar" #(1) 10 | ``` 11 | 12 | 1. Optionally define the filename, useful if it cannot be inferred from the url 13 | 14 | **Fields:** 15 | 16 | | Name | Type | Description | 17 | | ---------- | ------- | ------------------------------------------------------------------------ | 18 | | `type` | `"url"` | | 19 | | `url` | string | URL to the file | 20 | | `filename` | string? | Optional filename if you dont like the name from the url | 21 | | `desc` | string? | Optional description (shown in [markdown](../markdown-options.md)) | 22 | -------------------------------------------------------------------------------- /docs/reference/downloadable/github-releases.md: -------------------------------------------------------------------------------- 1 | # Github Releases 2 | 3 | Download something from GitHub Releases 4 | 5 | !!! example 6 | ```toml 7 | type = "ghrel" 8 | repo = "ViaVersion/ViaVersion" 9 | tag = "4.7.0" 10 | asset = "ViaVersion-${tag}.jar" #(1)! 11 | ``` 12 | 13 | 1. The real asset name is `ViaVersion-4.7.0.jar`. 14 | 15 | !!! note 16 | The strings can contain variable syntax: 17 | 18 | - `${mcver}` or `${mcversion}` for the `mc_version` in [server.toml](../server.toml.md) (usable in `tag` and `asset`) 19 | - `${tag}`, `${release}` or `${version}` for the resolved github release version (usable in `asset`) 20 | 21 | !!! note 22 | For the `asset` field, its first checked if the given asset exists on the release. If it doesn't, it will pick the first asset whose filename contains the `asset` value 23 | 24 | **Fields:** 25 | 26 | | Name | Type | Description | 27 | | ------- | ----------------- | ------------------------------------------------------- | 28 | | `type` | `"ghrel"` | | 29 | | `repo` | string | Repository with its owner, like `"ParadigmMC/mcman"` | 30 | | `tag` | string/`"latest"` | The 'tag' (version number in most cases) of the release | 31 | | `asset` | string/`"first"` | The name of the asset | 32 | -------------------------------------------------------------------------------- /docs/reference/downloadable/hangar.md: -------------------------------------------------------------------------------- 1 | # Hangar 2 | 3 | Downloads a plugin from [Hangar](https://hangar.papermc.io/) 4 | 5 | !!! example 6 | ```toml 7 | type = "hangar" 8 | id = "kennytv/Maintenance" #(1)! 9 | version = "latest" 10 | ``` 11 | 12 | 1. You can just use the project's slug here too 13 | 14 | **Fields:** 15 | 16 | | Name | Type | Description | 17 | | --------- | ----------------- | --------------------------------------------------------- | 18 | | `type` | `"hangar"` | | 19 | | `id` | string | The slug/name of the project | 20 | | `version` | string/`"latest"` | Version name, `"latest"` to always use the latest version | 21 | -------------------------------------------------------------------------------- /docs/reference/downloadable/index.md: -------------------------------------------------------------------------------- 1 | # Downloadable 2 | 3 | A `Downloadable` is a plugin, mod, datapack or any other thing that is, downloadable. 4 | 5 | Downloadable types are used for downloading custom server jars, plugins, mods, client-side mods, datapacks and worlds. 6 | 7 | ## Shortcode Syntax 8 | 9 | For some arguments, you can also use the "shortcode syntax": `source:id[,version]` 10 | 11 | Sources: 12 | 13 | - Modrinth: `modrinth`, `mr` 14 | - Curserinth: `curserinth`, `cr`, `cf`, `curseforge` 15 | - Hangar: `hangar`, `h` 16 | - Github: `gh`, `ghrel`, `github` 17 | 18 | ## Sources 19 | 20 | Please note that the `attributes` are not limitations. 21 | 22 | - [Modrinth](./modrinth.md) - `mods`, `plugins` and `datapacks` 23 | - [Spigot](./spigot.md) resources - `plugins` 24 | - [CurseRinth](./curserinth.md), curseforge api - `mods` 25 | - [Hangar](./hangar.md) - `plugins` 26 | - [Github Releases](./github-releases.md) - `*` 27 | - [Jenkins](./jenkins.md) - `*` 28 | - [Maven](./maven.md) - `*` 29 | - [Custom URL](./custom-url.md) - `*` 30 | -------------------------------------------------------------------------------- /docs/reference/downloadable/jenkins.md: -------------------------------------------------------------------------------- 1 | # Jenkins 2 | 3 | Download an artifact from a [Jenkins](https://www.jenkins.io/) server. 4 | 5 | !!! example 6 | Example using [Scissors](https://github.com/AtlasMediaGroup/Scissors) 1.20.1: 7 | 8 | ```toml 9 | type = "jenkins" 10 | url = "https://ci.plex.us.org" 11 | job = "Scissors/1.20.1" 12 | 13 | # (1) 14 | build = "latest" 15 | artifact = "first" 16 | ``` 17 | 18 | 1. These are the default values and since they are optional, they can be removed. 19 | 20 | !!! info 21 | Nested jobs can be written using slashes. For example, if the URL was something like `/job/A/job/B`, the job field would be `A/B`. 22 | 23 | **Fields:** 24 | 25 | | Name | Type | Description | 26 | | --- | --- | --- | 27 | | `type` | `"jenkins"`|| 28 | | `url` | string | URL to the Jenkins instance | 29 | | `job` | string | The job name | 30 | | `build` | string/`"latest"` | The build number to use | 31 | | `artifact` | string/`"first"` | The name of the artifact (checks for inclusion, like [github releases](./github-releases.md)) | 32 | -------------------------------------------------------------------------------- /docs/reference/downloadable/maven.md: -------------------------------------------------------------------------------- 1 | # Maven 2 | 3 | > Added in 0.4.0 4 | 5 | Download from a [Maven](https://maven.apache.org/) instance. 6 | 7 | **Fields:** 8 | 9 | | Name | Type | Description | 10 | | ---------- | --------- | ----------------------------- | 11 | | `type` | `"maven"` | | 12 | | `url` | string | URL to the Maven instance | 13 | | `group` | string | The group, seperated with `.` | 14 | | `artifact` | string | The name of the artifact | 15 | | `version` | string | The version of the artifact | 16 | | `filename` | string | Filename to download | 17 | 18 | !!! note 19 | The strings can contain variable syntax: 20 | 21 | - `${mcver}` or `${mcversion}` for the `mc_version` in [server.toml](../server.toml.md) 22 | - `${artifact}` for the resolved artifact (in `version` or `filename`) 23 | - `${version}` for the resolved version (in `filename`) 24 | 25 | !!! note 26 | For the `version` field, its first checked if the given version exists on the artifact. If it doesn't, it will pick the first version that contains the contents of `version` 27 | 28 | This is also true for the `filename` field 29 | 30 | **For example:** 31 | 32 | ```md 33 | Lets assume these are the versions and their files: 34 | 35 | - 1.19.4-1.0.0 36 | - amongus-1.19.4.jar 37 | - amongus-1.19.4-extra-sus.jar 38 | - 1.19.4-3.6.7 39 | - 3.6.7.jar 40 | 41 | And that: 42 | - artifact: "amongus" 43 | ``` 44 | 45 | | `mc_version` | maven `version` | resolved version | maven `filename` | resolved filename | 46 | | :----------- | :--------------- | :--------------- | :-------------------------- | :----------------------------- | 47 | | `1.19.4` | `${mcver}-1.0.0` | `1.19.4-1.0.0` | `${artifact}-${mcver}.jar` | `amongus-1.19.4.jar` | 48 | | `1.19.4` | `${mcver}` | `1.19.4-1.0.0` | `${artifact}-${mcver}-extr` | `amongus-1.19.4-extra-sus.jar` | 49 | | `1.19.4` | `${mcver}-3` | `1.19.4-3.6.7` | `${version}.jar` | `3.6.7.jar` | 50 | -------------------------------------------------------------------------------- /docs/reference/downloadable/modrinth.md: -------------------------------------------------------------------------------- 1 | # Modrinth 2 | 3 | Downloads a mod, plugin or a datapack from [Modrinth](https://modrinth.com/)'s API 4 | 5 | !!! example 6 | ```toml 7 | type = "modrinth" #(1)! 8 | id = "coreprotect" 9 | version = "mvLpRWww" #(2)! 10 | ``` 11 | 12 | 1. You can also use `mr` as an alias 13 | 2. You can find the version in the url of the download link or the version page. 14 | 15 | The 'version number' is also accepted (since mcman 0.3.0) 16 | 17 | **Fields:** 18 | 19 | | Name | Type | Description | 20 | | --------- | ------------------- | ---------------------------------------------------------- | 21 | | `type` | `"modrinth"`/`"mr"` | | 22 | | `id` | string | The slug or the ID of the project | 23 | | `version` | string/`"latest"` | Version ID or number, `"latest"` not recommended as of now | 24 | -------------------------------------------------------------------------------- /docs/reference/downloadable/spigot.md: -------------------------------------------------------------------------------- 1 | # Spigot 2 | 3 | Downloads a plugin from [Spiget](https://spiget.org/)'s API. 4 | 5 | You can find the ID of the resource in the URL: 6 | 7 | > `https://www.spigotmc.org/resources/luckperms.28140/` 8 | 9 | In this case, luckperms has the id of `28140` - but you can paste it with the name too: 10 | 11 | !!! example 12 | ```toml title="Download LuckPerms from spigot" 13 | type = "spigot" 14 | id = "luckperms.28140" 15 | ``` 16 | 17 | !!! tip 18 | mcman will ignore everything before the dot in the `id` field. This helps with identifying the plugins and should be easier to just copy-paste the id from the URL. 19 | 20 | **Fields:** 21 | 22 | | Name | Type | Description | 23 | | ------ | ---------- | ----------------- | 24 | | `type` | `"spigot"` | | 25 | | `id` | string | ID of the project | 26 | -------------------------------------------------------------------------------- /docs/reference/hook.md: -------------------------------------------------------------------------------- 1 | # Hook 2 | 3 | Hooks allow you to define scripts and other stuff that can run before or after certain tasks, such as [Building](../concepts/building.md), 4 | 5 | Define hooks in [server.toml](./server.toml.md) or [network.toml](./network.toml.md) 6 | -------------------------------------------------------------------------------- /docs/reference/lockfile.md: -------------------------------------------------------------------------------- 1 | # Lockfile (`.mcman.lock`) 2 | 3 | The lockfile, found under the output directory, is generated after every build. It's in the JSON format and contains metadata about the installed mods, plugins and last dates of config files. 4 | 5 | While it's primary purpose is to be a cache and speed up building, it also makes sure that the removed mods/plugins from the `server.toml` file also get their jar files deleted. 6 | 7 | ## Disabling 8 | 9 | See [Options/Disabling lockfiles](../concepts/options.md#disabling-lockfiles) 10 | 11 | ## Format 12 | 13 | ```ts 14 | type Lockfile = { 15 | plugins: [Downloadable, ResolvedFile][], 16 | mods: [Downloadable, ResolvedFile][], 17 | files: BootstrappedFile[], 18 | } 19 | 20 | type BootstrappedFile = { 21 | path: string, 22 | date: Timestamp, 23 | } 24 | ``` 25 | -------------------------------------------------------------------------------- /docs/reference/markdown-options.md: -------------------------------------------------------------------------------- 1 | # Markdown Options 2 | 3 | Ever wanted to display all of the mods or plugins your server has? Using `mcman` you can do that! 4 | 5 | If you havent added it, add your markdown file's name (`README.md` for example) into the `markdown.files` list. When the `mcman markdown` command is run, mcman will render every template in the listed files. 6 | 7 | ``` toml title="server.toml" 8 | [markdown] 9 | files = [ 10 | "README.md", 11 | "PLUGINS.md", 12 | ] 13 | auto_update = false 14 | ``` 15 | 16 | ## Fields 17 | 18 | `files`: string[] 19 | 20 | : List of filenames to 'render' 21 | 22 | `auto_update`: bool 23 | 24 | : If set to `true`, markdown files will be rendered on commands that modify `server.toml` 25 | 26 | !!! warning 27 | If `#!toml auto_update = true`, commands might take longer. We recommend you dont turn it on until you're done adding most of the mods/plugins. 28 | 29 | ## Markdown Templates 30 | 31 | These are the templates mcman will render inside your markdown files. When `mcman markdown` runs, the files specified in `server.toml` will be read and the templates below will be updated with the rendered markdown code. You can have as many markdown files or templates as you want. 32 | 33 | ### Server Info Table 34 | 35 | This template renders a table with server jar info. 36 | 37 | ``` md title="README.md" 38 | 39 | ... content ... 40 | 41 | ``` 42 | 43 | !!! note "Example render:" 44 | | Version | Type | Build | 45 | | ------- | ------------------------------------------ | -------- | 46 | | 1.20.1 | [Paper](https://papermc.io/software/paper) | *Latest* | 47 | 48 | ### Addons List 49 | 50 | This template renders a list of addons (plugins or mods) 51 | 52 | ```md title="README.md" 53 | 54 | ... content ... 55 | 56 | ``` 57 | 58 | !!! note "Example render:" 59 | | Name | Description | 60 | | --- | --- | 61 | | [BlueMap](https://modrinth.com/plugin/bluemap) | A Minecraft mapping tool that creates 3D models of your Minecraft worlds and displays them in a web viewer. | 62 | | [FastAsyncWorldEdit](https://modrinth.com/plugin/fastasyncworldedit) | Blazingly fast world manipulation for artists, builders and everyone else | 63 | -------------------------------------------------------------------------------- /docs/reference/network.toml.md: -------------------------------------------------------------------------------- 1 | # `network.toml` 2 | 3 | The `network.toml` file defines a Network of multiple servers, such as BungeeCord or Velocity based. 4 | 5 | ```toml 6 | name = "SuperCraft" 7 | proxy = "proxy" 8 | port = 25565 9 | 10 | [servers.lobby] 11 | port = 25566 12 | 13 | [servers.game1] 14 | port = 25567 15 | 16 | [variables] 17 | MOTD = "Welcome to SuperCraft!" 18 | ``` 19 | 20 | ## Folder Structure 21 | 22 | The folder structure should look something like this: 23 | 24 | ```yaml 25 | . 26 | ├─ network.toml 27 | └─ servers/ 28 | ├─ server1 29 | │ └─ server.toml 30 | └─ server2 31 | └─ server.toml 32 | ``` 33 | 34 | Have a folder `servers` next to `network.toml` and create a folder for each `server.toml` 35 | 36 | ## Fields 37 | 38 | `name`: string 39 | 40 | : Defines the name of this network. You can access this in config files using `${NETWORK_NAME}` 41 | 42 | `proxy`: string 43 | 44 | : Folder name of the proxy server, currently unused. 45 | 46 | `port`: number 47 | 48 | : The port this network (or rather its proxy) 49 | 50 | `variables`: table 51 | 52 | : This field defines variables global to the network. They can be accessed by prefixing the variable name with `NW_`: 53 | 54 | ``` toml title="network.toml" 55 | [variables] 56 | MOTD = "hello world" 57 | ``` 58 | 59 | ``` properties title="servers/game1/server.properties" 60 | motd=${NW_MOTD} 61 | ``` 62 | 63 | `servers`: table of [ServerEntry](#serverentry) 64 | 65 | : In this table, you define your servers the network has 66 | 67 | ## ServerEntry 68 | 69 | ```toml 70 | [servers.dennis_smp] 71 | port = 25566 72 | ip_address = "127.0.0.1" 73 | ``` 74 | 75 | **Fields:** 76 | 77 | `port`: number 78 | 79 | : Defines the port of the server. In the server configuration files you can use `${SERVER_PORT}` to access this. 80 | 81 | You can also override the value by defining `PORT_name` system environment variable where `name` is name of the server entry. 82 | 83 | `ip_address`: string 84 | 85 | : Optionally define the IP address of the server. This is `"127.0.0.1"` by default. 86 | 87 | Similarly to `port`, you can override this using the `IP_name` system environment variable. 88 | 89 | 90 | -------------------------------------------------------------------------------- /docs/reference/server-launcher.md: -------------------------------------------------------------------------------- 1 | # Server Launcher 2 | 3 | The `[launcher]` table in [`server.toml`](./server.toml.md) lets mcman create launch scripts for you. 4 | 5 | The scripts are named `start.sh` and `start.bat` and are created inside the output directory, `server/`. 6 | 7 | The order of these arguments are: 8 | 9 | `java [jvm_args] [memory] [preset] [eula] [properties] [nogui] [game_args]` 10 | 11 | Where `startup` is either `-jar *.jar` or some library shenanigans (NeoForge/Forge require this). 12 | 13 | ??? example "Example ServerLauncher" 14 | 15 | ``` toml 16 | [launcher] 17 | disable = false # (1) 18 | 19 | # (2) 20 | jvm_args = "-exampleidk" 21 | game_args = "--world abc" 22 | 23 | aikars_flags = true # (3) 24 | proxy_flags = false # (4) 25 | 26 | eula_args = true # (5) 27 | 28 | nogui = true # (6) 29 | 30 | memory = "2048M" # (7) 31 | 32 | # (8) 33 | [launcher.properties] 34 | hello="thing" 35 | ``` 36 | 37 | 1. Disables generating launch scripts completely. `false` by default 38 | 2. If needed, you can add custom arguments here. The format is `java [jvm_args] -jar server.jar [game_args]` 39 | 3. Use aikar's flags - these do optimizations, see [flags.sh](https://flags.sh) for more info 40 | 4. Like aikar's, but for proxies (bungeecord, waterfall, velocity) 41 | 5. Adds `-Dcom.mojang.eula.agree=true` - this flag exists in spigot/paper to ignore `eula.txt`. Writes to `eula.txt` when on fabric or quilt 42 | 6. Adds `--nogui` to game args, disable if its a proxy server as they dont support it 43 | 7. Specify `-Xmx`/`-Xms` (memory) for the server. 44 | 8. A table of properties. This is the same as using 45 | ``` toml 46 | jvm_args = "-Dhello=thing" 47 | ``` 48 | 49 | ## Fields 50 | 51 | `disable`: bool 52 | 53 | : If set to true, mcman will not generate start scripts 54 | 55 | `memory`: string 56 | 57 | : Amount of memory to give, in jvm byte units. These are set using the `-Xmx`/`-Xms` arguments. 58 | 59 | For example, `2048M` 60 | 61 | You can also override this using the `MC_MEMORY` environment variable while building. 62 | 63 | `java_version`: string 64 | 65 | : This field does not add any arguments to the startup command but rather helps mcman decide which java binary to use. 66 | 67 | ```toml 68 | [launcher] 69 | java_version = "17" 70 | ``` 71 | 72 | See [this section](../tutorials/options.md#setting-the-java-binary) for more information. 73 | 74 | `nogui`: bool 75 | 76 | : Adds `--nogui` to the end as a game argument. Set this to false for proxy servers since they dont support it. 77 | 78 | `preset_flags`: PresetFlags 79 | 80 | : Select a preset. Available presets are: 81 | 82 | - `none` (default) 83 | - `aikars`: Use [Aikar's Flags](https://mcflags.emc.gs), there's also [a post by PaperMC](https://docs.papermc.io/paper/aikars-flags) about it 84 | - `proxy`: Preset for proxy servers 85 | 86 | `eula_args`: bool 87 | 88 | : Bukkit/Spigot forks such as Paper and Purpur all support the `-Dcom.mojang.eula.agree=true` system property flag which allows the agreement of eula without `eula.txt`. If this is set to **`true`**, mcman will add this flag to the arguments. If the server software does not support this argument (such as fabric), `eula=true` will be written to `eula.txt`. 89 | 90 | `jvm_args`: string 91 | 92 | : Add JVM arguments here. JVM arguments are generally in the format of `-X...` and are entered before `-jar server.jar` 93 | 94 | `game_args`: string 95 | 96 | : Game arguments are entered *after* `-jar server.jar` and are picked up by the server process. 97 | 98 | `properties`: Map 99 | 100 | : Enter a table of system property arguments. Properties are in the format of `-Dkey=value`. 101 | 102 | For example, to write `-Dterminal.jline=false -Dterminal.ansi=true`: 103 | ```toml 104 | [launcher.properties] 105 | terminal.jline=false 106 | terminal.ansi=true 107 | ``` 108 | 109 | -------------------------------------------------------------------------------- /docs/reference/server.toml.md: -------------------------------------------------------------------------------- 1 | # Server (`server.toml`) 2 | 3 | Each `server.toml` file defines a differient server. 4 | 5 | To generate one, you can use the [`mcman init`](../commands/init.md) command (see [Getting Started](../concepts/getting-started.md)) 6 | 7 | !!! note 8 | If you are in your server's sub-directories, mcman will be able to find the `server.toml` file recursively. 9 | 10 | ```toml 11 | name = "My SMP" 12 | mc_version = "1.20.1" 13 | 14 | [jar] 15 | type = "purpur" 16 | ``` 17 | 18 | ## Fields 19 | 20 | `name`: string 21 | 22 | : The name field defines the name of the server. It's recommended to be alphanumeric because of the other features using this field. 23 | 24 | For example, to [overwrite](../tutorials/options.md#overriding-server-ports-in-networks) the `SERVER_PORT` variable, you can use the `PORT_name` environment variable where `name` is the server's name. 25 | 26 | The [`SERVER_NAME` variable](../tutorials/variables.md#special-variables) can be used to access this field. 27 | 28 | `mc_version`: string 29 | 30 | : The mc_version field is used in many ways, most notably: 31 | 32 | - Selecting the server jar 33 | - Filtering addon versions 34 | 35 | This field can also be accessed using the `SERVER_VERSION`, `mcversion` or `mcver` variables. 36 | 37 | `jar`: [ServerType](./servertype/index.md) 38 | 39 | : In the `[jar]` section, you define what kind of server, such as Fabric or Purpur, you want to use. 40 | 41 | See [ServerType](./servertype/index.md) for a list of options you have. 42 | 43 | `launcher`: [ServerLauncher](./server-launcher.md) 44 | 45 | : This section defines the [start scripts](./server-launcher.md) which you can configure or disable. 46 | 47 | `plugins`: [Downloadable](./downloadable/index.md)[] 48 | 49 | : A list of plugins that mcman should download. Plugins are kept under `server/plugins/` 50 | 51 | `mods`: [Downloadable](./downloadable/index.md)[] 52 | 53 | : A list of mods that mcman should download. Mods are kept under `server/mods/` 54 | 55 | `variables`: Map 56 | 57 | : Define server variables to use in `config/` here. For more information about how this works, read the [Bootstrapping](../concepts/variables.md) section. 58 | 59 | `worlds`: Map 60 | 61 | : A table of [World](./world.md)s. [How can I use worlds?](../concepts/using-worlds.md) 62 | 63 | `markdown`: [MarkdownOptions](./markdown-options.md) 64 | 65 | : Configure rendering markdown about your server using [Markdown Options](./markdown-options.md) 66 | -------------------------------------------------------------------------------- /docs/reference/servertype/buildtools.md: -------------------------------------------------------------------------------- 1 | # BuildTools 2 | 3 | Setup Spigot or CraftBukkit using [BuildTools](https://www.spigotmc.org/wiki/buildtools/). 4 | 5 | !!! note 6 | `mcman` will need to run `java` to install the server, ensure it exists in the environment before building 7 | 8 | !!! example 9 | ```toml 10 | type = "buildtools" 11 | software = "craftbukkit" 12 | ``` 13 | 14 | **Fields:** 15 | 16 | | Name | Type | Description | 17 | | ---------- | ----------------------------- | -------------------------------------------------------- | 18 | | `type` | `"buildtools"` | | 19 | | `software` | `"spigot"` or `"craftbukkit"` | The software to compile | 20 | | `args` | string[] | Additional args. mcman already adds `--rev {mc_version}` | 21 | -------------------------------------------------------------------------------- /docs/reference/servertype/bungeecord.md: -------------------------------------------------------------------------------- 1 | # BungeeCord 2 | 3 | BungeeCord is just a shortcut to a [jenkins](../downloadable/jenkins.md) downloadable: 4 | 5 | ```toml 6 | type = "bungeecord" 7 | ``` 8 | 9 | !!! note 10 | If you'd like to get a specific build, use this: 11 | 12 | ```toml 13 | type = "jenkins" 14 | url = "https://ci.md-5.net" 15 | job = "BungeeCord" 16 | build = "latest" #(1) 17 | artifact = "BungeeCord" 18 | ``` 19 | 20 | 1. Change this to the build id of your choosing 21 | -------------------------------------------------------------------------------- /docs/reference/servertype/fabric.md: -------------------------------------------------------------------------------- 1 | # Fabric 2 | 3 | Downloads the server jar from [Fabric](https://fabricmc.net/) 4 | 5 | **Fields:** 6 | 7 | | Name | Type | Description | 8 | | --- | --- | --- | 9 | | `type` | `"fabric"` | | 10 | | `installer` | string/`"latest"` | Installer version to use 11 | | `loader` | string/`"latest"` | Loader version to use 12 | 13 | ```toml 14 | type = "fabric" 15 | installer = "latest" 16 | loader = "latest" 17 | ``` 18 | -------------------------------------------------------------------------------- /docs/reference/servertype/forge.md: -------------------------------------------------------------------------------- 1 | # Forge 2 | 3 | Downloads the server jar from [Forge](https://forums.minecraftforge.net/) 4 | 5 | !!! note 6 | `mcman` will need to run `java` to install the server, ensure it exists in the environment before building 7 | 8 | **Fields:** 9 | 10 | | Name | Type | Description | 11 | | -------- | ----------------- | --------------------- | 12 | | `type` | `"forge"` | | 13 | | `loader` | string/`"latest"` | Loader version to use | 14 | 15 | ```toml 16 | type = "forge" 17 | loader = "latest" 18 | ``` 19 | -------------------------------------------------------------------------------- /docs/reference/servertype/index.md: -------------------------------------------------------------------------------- 1 | # Server Types 2 | 3 | A server type is like a [Downloadable](../downloadable/index.md) but for server softwares. 4 | 5 | !!! success "Downloadable Support" 6 | The Server Type 'inherits' [Downloadable](../downloadable/index.md) meaning any valid Downloadable can be used as a Server Type 7 | 8 | ## Index 9 | 10 | - [Vanilla](../servertype/vanilla.md) 11 | - [Quilt](../servertype/quilt.md) - `modded` 12 | - [Fabric](../servertype/fabric.md) - `modded` 13 | - [NeoForge](../servertype/neoforge.md) - `modded` 14 | - [Forge](../servertype/forge.md) - `modded` 15 | - [PaperMC](../servertype/papermc.md) 16 | - [Paper](../servertype/papermc.md#shortcuts) `bukkit` 17 | - [Velocity](../servertype/papermc.md#shortcuts) `proxy` 18 | - [Waterfall](../servertype/papermc.md#shortcuts) `proxy` 19 | - [Purpur](./purpurmc.md) - `bukkit` 20 | - [BungeeCord](./bungeecord.md) - `proxy` 21 | - [BuildTools](./buildtools.md) (spigot and craftbukkit) - `bukkit` 22 | - ... any other [Downloadable](../downloadable/index.md) 23 | -------------------------------------------------------------------------------- /docs/reference/servertype/neoforge.md: -------------------------------------------------------------------------------- 1 | # NeoForge 2 | 3 | Downloads the server jar from [NeoForge](https://neoforged.net/) 4 | 5 | !!! note 6 | `mcman` will need to run `java` to install the server, ensure it exists in the environment before building 7 | 8 | **Fields:** 9 | 10 | | Name | Type | Description | 11 | | -------- | ----------------- | --------------------- | 12 | | `type` | `"neoforge"` | | 13 | | `loader` | string/`"latest"` | Loader version to use | 14 | 15 | ```toml 16 | type = "neoforge" 17 | loader = "latest" 18 | ``` 19 | -------------------------------------------------------------------------------- /docs/reference/servertype/papermc.md: -------------------------------------------------------------------------------- 1 | # PaperMC 2 | 3 | Downloads a [PaperMC](https://papermc.io/) project. 4 | 5 | !!! example 6 | ```toml title="PaperMC Downloadable" 7 | type = "papermc" 8 | project = "waterfall" 9 | build = "17" 10 | ``` 11 | 12 | ??? tip "Shortcuts" 13 | 14 | There are also 3 shortcut Downloadable types: 15 | 16 | - `paper` 17 | - `velocity` 18 | - `waterfall` 19 | 20 | ```toml title="Example shortcut" 21 | type = "paper" 22 | ``` 23 | 24 | !!! note 25 | The shortcuts dont support the `build` property. They are implicitly the latest build. 26 | 27 | 28 | 29 | **Fields:** 30 | 31 | | Name | Type | Description | 32 | | --------- | ----------------- | ------------------------------- | 33 | | `type` | `"papermc"` | | 34 | | `project` | string | The project name | 35 | | `build` | string/`"latest"` | Optionally provide the build id | 36 | -------------------------------------------------------------------------------- /docs/reference/servertype/purpurmc.md: -------------------------------------------------------------------------------- 1 | # PurpurMC 2 | 3 | Downloads the server jar from [PurpurMC](https://purpurmc.org/). 4 | 5 | !!! example 6 | ```toml 7 | type = "purpur" 8 | build = "10" #(1)! 9 | ``` 10 | 11 | 1. Defaults to `latest` 12 | 13 | **Fields:** 14 | 15 | | Name | Type | Description | 16 | | ------- | ----------------- | ------------------------------ | 17 | | `type` | `"purpur"` | | 18 | | `build` | string/`"latest"` | Optionally define the build id | 19 | -------------------------------------------------------------------------------- /docs/reference/servertype/quilt.md: -------------------------------------------------------------------------------- 1 | # Quilt 2 | 3 | Downloads [Quilt](https://quiltmc.org/) installer and installs the quilt server. 4 | 5 | !!! note 6 | `mcman` will need to run `java` to install the quilt server jar, ensure it exists in the environment before building 7 | 8 | !!! example 9 | ```toml 10 | type = "quilt" 11 | installer = "latest" 12 | loader = "latest" 13 | ``` 14 | 15 | **Fields:** 16 | 17 | | Name | Type | Description | 18 | | ----------- | --------------------------------- | ------------------------ | 19 | | `type` | `"quilt"` | | 20 | | `installer` | string/`"latest"` | Installer version to use | 21 | | `loader` | string/`"latest-beta"`/`"latest"` | Loader version to use | 22 | -------------------------------------------------------------------------------- /docs/reference/servertype/vanilla.md: -------------------------------------------------------------------------------- 1 | # Vanilla 2 | 3 | Used for a vanilla server jar. Has no properties because it only needs the server version, which would be defined in `mc_version` ([`server.toml`](../server.toml.md)) 4 | 5 | ```toml 6 | type = "vanilla" 7 | ``` 8 | -------------------------------------------------------------------------------- /docs/reference/world.md: -------------------------------------------------------------------------------- 1 | # World 2 | 3 | > Added in v0.2.2 4 | 5 | Represents a world in your server. Currently only exists for datapack support. 6 | 7 | This is a simple type - it only contains a list of [Downloadable](./downloadable)s. 8 | 9 | Worlds are indexed by their name in `server.toml`'s `worlds` table. 10 | 11 | ```toml title="server.toml" 12 | [worlds.skyblock] 13 | datapacks = [] 14 | ``` 15 | 16 | **Fields:** 17 | 18 | | Name | Type | Description | 19 | | --- | --- | --- | 20 | | `datapacks` | [Downloadable[]](./downloadable/index.md) | A list of datapacks to download for this world | 21 | -------------------------------------------------------------------------------- /examples/creative-anarchy/README.md: -------------------------------------------------------------------------------- 1 | # creative-anarchy 2 | 3 | [![mcman badge](https://img.shields.io/badge/uses-mcman-purple?logo=github)](https://github.com/ParadigmMC/mcman) 4 | 5 | A creative server demonstrating: 6 | 7 | - Custom server jars 8 | - Hangar Plugins 9 | 10 | 11 | 12 | 13 | | Version | Type | Build | 14 | | ------- | ----------------------------------------------------------------- | -------- | 15 | | 1.20.1 | [Scissors/1.20.1](https://ci.plex.us.org/job/Scissors/job/1.20.1) | *Latest* | 16 | 17 | 18 | ## Plugins 19 | 20 | 21 | | Name | Description | Version | 22 | | ------------------------------------------------------ | ------------------------------------------------------------------------------------------------ | ------- | 23 | | [ChessCraft](https://hangar.papermc.io/jmp/ChessCraft) | Paper plugin adding in-world chess matches against players and chess engines (CPUs) to Minecraft | latest | 24 | -------------------------------------------------------------------------------- /examples/creative-anarchy/config/server.properties: -------------------------------------------------------------------------------- 1 | server-port=${PORT:25565} 2 | motd=${SERVER_NAME:A Minecraft Server} 3 | gamemode=creative 4 | -------------------------------------------------------------------------------- /examples/creative-anarchy/hotreload.toml: -------------------------------------------------------------------------------- 1 | [[files]] 2 | path = "server.properties" 3 | action = "Reload" 4 | 5 | [[files]] 6 | path = "spigot.yml" 7 | action = "/say sussy" 8 | -------------------------------------------------------------------------------- /examples/creative-anarchy/server.toml: -------------------------------------------------------------------------------- 1 | name = "creative-anarchy" 2 | mc_version = "1.20.2" 3 | 4 | [jar] 5 | type = "jenkins" 6 | url = "https://ci.plex.us.org" 7 | job = "Scissors/${mcversion}" 8 | build = "latest" 9 | artifact = "first" 10 | 11 | [variables] 12 | PORT = "25565" 13 | 14 | [launcher] 15 | aikars_flags = true 16 | proxy_flags = false 17 | nogui = true 18 | eula_args = true 19 | 20 | [markdown] 21 | files = ["README.md"] 22 | auto_update = false 23 | 24 | [worlds.world] 25 | -------------------------------------------------------------------------------- /examples/creative-anarchy/worlds/world.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ParadigmMC/mcman/00668083634c6d20b96dbc7c5f5dd21615ed244c/examples/creative-anarchy/worlds/world.zip -------------------------------------------------------------------------------- /examples/datapacks/README.md: -------------------------------------------------------------------------------- 1 | # datapacks 2 | 3 | [![mcman badge](https://img.shields.io/badge/uses-mcman-purple?logo=github)](https://github.com/ParadigmMC/mcman) 4 | 5 | 6 | 7 | 8 | | Version | Type | 9 | | ------- | ------- | 10 | | 1.20.1 | Vanilla | 11 | 12 | 13 | ## Plugins 14 | 15 | 16 | | | 17 | | | 18 | -------------------------------------------------------------------------------- /examples/datapacks/config/server.properties: -------------------------------------------------------------------------------- 1 | server-port=${PORT:25565} 2 | motd=${SERVER_NAME:A Minecraft Server} 3 | -------------------------------------------------------------------------------- /examples/datapacks/server.toml: -------------------------------------------------------------------------------- 1 | name = "datapacks" 2 | mc_version = "1.20.1" 3 | 4 | [jar] 5 | type = "vanilla" 6 | 7 | [variables] 8 | PORT = "25565" 9 | 10 | [launcher] 11 | aikars_flags = true 12 | proxy_flags = false 13 | nogui = true 14 | eula_args = true 15 | 16 | [[worlds.world.datapacks]] 17 | type = "modrinth" 18 | id = "vanilla-refresh" 19 | version = "c1SnJlLJ" 20 | 21 | [markdown] 22 | files = ["README.md"] 23 | auto_update = false 24 | -------------------------------------------------------------------------------- /examples/forge/README.md: -------------------------------------------------------------------------------- 1 | # forge 2 | 3 | [![mcman badge](https://img.shields.io/badge/uses-mcman-purple?logo=github)](https://github.com/ParadigmMC/mcman) 4 | 5 | 6 | 7 | 8 | 9 | 10 | ## Mods 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /examples/forge/config/server.properties: -------------------------------------------------------------------------------- 1 | server-port=${PORT:25565} 2 | motd=${SERVER_NAME:A Minecraft Server} 3 | -------------------------------------------------------------------------------- /examples/forge/server.toml: -------------------------------------------------------------------------------- 1 | name = "forge" 2 | mc_version = "1.7.10" 3 | 4 | [jar] 5 | type = "forge" 6 | loader = "latest" 7 | 8 | [variables] 9 | PORT = "25565" 10 | 11 | [launcher] 12 | eula_args = true 13 | nogui = true 14 | preset_flags = "aikars" 15 | 16 | [options] 17 | -------------------------------------------------------------------------------- /examples/mrpack/README.md: -------------------------------------------------------------------------------- 1 | # myserver 2 | 3 | [![mcman badge](https://img.shields.io/badge/uses-mcman-purple?logo=github)](https://github.com/ParadigmMC/mcman) 4 | 5 | 6 | 7 | 8 | 9 | 10 | ## Mods 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /examples/mrpack/config/config/yosbr/config/c2me.toml: -------------------------------------------------------------------------------- 1 | version = 3 2 | # (Default: 2) Configures the parallelism of global executor. 3 | globalExecutorParallelism = "2" 4 | 5 | [threadedWorldGen] 6 | # (Default: false) Whether to enable this feature 7 | enabled = false -------------------------------------------------------------------------------- /examples/mrpack/config/config/yosbr/config/ferritecore.mixin.properties: -------------------------------------------------------------------------------- 1 | # Replace the blockstate neighbor table 2 | replaceNeighborLookup = true 3 | # Do not store the properties of a state explicitly and read themfrom the replace neighbor table instead. Requires replaceNeighborLookup to be enabled 4 | replacePropertyMap = true 5 | # Cache the predicate instances used in multipart models 6 | cacheMultipartPredicates = true 7 | # Avoid creation of new strings when creating ModelResourceLocations 8 | modelResourceLocations = true 9 | # Do not create a new MultipartBakedModel instance for each block state using the same multipartmodel. Requires cacheMultipartPredicates to be enabled 10 | multipartDeduplication = true 11 | # Deduplicate cached data for blockstates, most importantly collision and render shapes 12 | blockstateCacheDeduplication = true 13 | # Deduplicate vertex data of baked quads in the basic model implementations 14 | bakedQuadDeduplication = true 15 | # Replace objects used to detect multi-threaded access to chunks by a much smaller field. This option is disabled by default due to very rare and very hard-to-reproduce crashes, use at your own risk! 16 | useSmallThreadingDetector = true 17 | # Use a slightly more compact, but also slightly slower representation for block states 18 | compactFastMap = false 19 | # Populate the neighbor table used by vanilla. Enabling this slightly increases memory usage, but can help with issues in the rare case where mods access it directly. 20 | populateNeighborTable = false 21 | -------------------------------------------------------------------------------- /examples/mrpack/config/config/yosbr/config/servercore.toml: -------------------------------------------------------------------------------- 1 | # Lets you enable / disable certain features and modify them. 2 | [features] 3 | # (Default = false) Makes villagers tick less often if they are stuck in a 1x1 space. 4 | lobotomize_villagers = true 5 | # (Default = 0.5) Decides the radius in blocks that items / xp will merge at. 6 | item_merge_radius = 0.75 7 | xp_merge_radius = 1.0 8 | 9 | # Allows you to toggle specific optimizations that don't have full vanilla parity. 10 | # These settings will only take effect after server restarts. 11 | [optimizations] 12 | # (Default = false) Can significantly reduce time spent on mobspawning, but isn't as accurate as vanilla on biome borders. 13 | # This may cause mobs from another biome to spawn a few blocks across a biome border (this does not affect structure spawning!). 14 | fast_biome_lookups = true 15 | -------------------------------------------------------------------------------- /examples/mrpack/config/config/yosbr/config/threadtweak.json: -------------------------------------------------------------------------------- 1 | { 2 | "threadPriority": { 3 | "game": 8, 4 | "main": 3, 5 | "io": 4, 6 | "integratedServer": 8 7 | } 8 | } -------------------------------------------------------------------------------- /examples/mrpack/config/config/yosbr/config/vmp.properties: -------------------------------------------------------------------------------- 1 | # Configuration file for VMP 2 | show_async_loading_messages=false -------------------------------------------------------------------------------- /examples/mrpack/config/server.properties: -------------------------------------------------------------------------------- 1 | server-port=${PORT:25565} 2 | motd=${SERVER_NAME:A Minecraft Server} 3 | -------------------------------------------------------------------------------- /examples/mrpack/server.toml: -------------------------------------------------------------------------------- 1 | name = "myserver" 2 | mc_version = "1.20.4" 3 | 4 | [jar] 5 | type = "fabric" 6 | loader = "0.15.3" 7 | installer = "latest" 8 | 9 | [variables] 10 | PORT = "25565" 11 | 12 | [launcher] 13 | eula_args = true 14 | nogui = true 15 | 16 | [markdown] 17 | files = ["README.md"] 18 | auto_update = false 19 | 20 | [options] 21 | 22 | [[mods]] 23 | type = "modrinth" 24 | id = "uXXizFIs" 25 | version = "pguEMpy9" 26 | [[mods]] 27 | type = "modrinth" 28 | id = "P7dR8mSH" 29 | version = "JMCwDuki" 30 | 31 | [[mods]] 32 | type = "modrinth" 33 | id = "wnEe9KBa" 34 | version = "7ORmCVy9" 35 | -------------------------------------------------------------------------------- /examples/neoforge/README.md: -------------------------------------------------------------------------------- 1 | # neoforge 2 | 3 | [![mcman badge](https://img.shields.io/badge/uses-mcman-purple?logo=github)](https://github.com/ParadigmMC/mcman) 4 | 5 | 6 | 7 | 8 | 9 | 10 | ## Mods 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /examples/neoforge/config/server.properties: -------------------------------------------------------------------------------- 1 | server-port=${PORT:25565} 2 | motd=${SERVER_NAME:A Minecraft Server} 3 | -------------------------------------------------------------------------------- /examples/neoforge/server.toml: -------------------------------------------------------------------------------- 1 | name = "neoforge" 2 | mc_version = "1.20.1" 3 | 4 | [jar] 5 | type = "neoforge" 6 | loader = "latest" 7 | 8 | [variables] 9 | PORT = "25565" 10 | 11 | [launcher] 12 | eula_args = true 13 | nogui = true 14 | preset_flags = "aikars" 15 | 16 | [options] 17 | -------------------------------------------------------------------------------- /examples/network/README.md: -------------------------------------------------------------------------------- 1 | # CoolNetwork 2 | 3 | [![mcman badge](https://img.shields.io/badge/uses-mcman-purple?logo=github)](https://github.com/ParadigmMC/mcman) 4 | 5 | 6 | 7 | ## Servers 8 | 9 | 10 | | Name | Port | 11 | | --------------------------- | ----- | 12 | | [`lobby`](./servers/lobby/) | 25566 | 13 | | [`game1`](./servers/game1/) | 25567 | 14 | 15 | -------------------------------------------------------------------------------- /examples/network/network.toml: -------------------------------------------------------------------------------- 1 | name = "CoolNetwork" 2 | proxy = "proxy" 3 | port = 25565 4 | 5 | [servers.lobby] 6 | port = 25566 7 | 8 | [servers.game1] 9 | port = 25567 10 | 11 | [variables] 12 | MADE_USING = "mcman" 13 | MOTD = "Welcome to CoolNetwork!" 14 | 15 | [markdown] 16 | files = ["README.md"] 17 | auto_update = false 18 | -------------------------------------------------------------------------------- /examples/network/servers/game1/README.md: -------------------------------------------------------------------------------- 1 | # game1 2 | 3 | [![mcman badge](https://img.shields.io/badge/uses-mcman-purple?logo=github)](https://github.com/ParadigmMC/mcman) 4 | 5 | 6 | 7 | 8 | 9 | 10 | ## Mods 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /examples/network/servers/game1/config/server.properties: -------------------------------------------------------------------------------- 1 | server-port=${PORT} 2 | motd=${NW_MADE_USING} 3 | -------------------------------------------------------------------------------- /examples/network/servers/game1/server.toml: -------------------------------------------------------------------------------- 1 | name = "game1" 2 | mc_version = "1.20.2" 3 | 4 | [jar] 5 | type = "quilt" 6 | loader = "latest" 7 | installer = "latest" 8 | 9 | [variables] 10 | PORT = "25565" 11 | 12 | [launcher] 13 | nogui = true 14 | preset_flags = "aikars" 15 | eula_args = true 16 | 17 | [options] 18 | upload_to_mclogs = false 19 | -------------------------------------------------------------------------------- /examples/network/servers/lobby/README.md: -------------------------------------------------------------------------------- 1 | # lobby 2 | 3 | [![mcman badge](https://img.shields.io/badge/uses-mcman-purple?logo=github)](https://github.com/ParadigmMC/mcman) 4 | 5 | 6 | 7 | 8 | 9 | 10 | ## Plugins 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /examples/network/servers/lobby/config/server.properties: -------------------------------------------------------------------------------- 1 | server-port=${PORT:25565} 2 | motd=${SERVER_NAME:A Minecraft Server} 3 | -------------------------------------------------------------------------------- /examples/network/servers/lobby/server.toml: -------------------------------------------------------------------------------- 1 | name = "lobby" 2 | mc_version = "1.20.2" 3 | 4 | [jar] 5 | type = "paper" 6 | build = "latest" 7 | 8 | [variables] 9 | PORT = "25565" 10 | 11 | [launcher] 12 | nogui = true 13 | preset_flags = "aikars" 14 | eula_args = true 15 | 16 | [options] 17 | upload_to_mclogs = false 18 | -------------------------------------------------------------------------------- /examples/network/servers/proxy/README.md: -------------------------------------------------------------------------------- 1 | # proxy 2 | 3 | [![mcman badge](https://img.shields.io/badge/uses-mcman-purple?logo=github)](https://github.com/ParadigmMC/mcman) 4 | 5 | 6 | 7 | 8 | | Version | Type | 9 | | ------- | ------------------------------------------------ | 10 | | latest | [Velocity](https://papermc.io/software/velocity) | 11 | 12 | 13 | ## Plugins 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /examples/network/servers/proxy/config/velocity.toml: -------------------------------------------------------------------------------- 1 | # Config version. Do not change this 2 | config-version = "2.6" 3 | bind = "0.0.0.0:25577" 4 | motd = "${NW_MOTD}" 5 | show-max-players = 500 6 | online-mode = false 7 | force-key-authentication = true 8 | prevent-client-proxy-connections = false 9 | player-info-forwarding-mode = "modern" 10 | forwarding-secret-file = "forwarding.secret" 11 | announce-forge = false 12 | kick-existing-players = false 13 | ping-passthrough = "DISABLED" 14 | enable-player-address-logging = true 15 | 16 | [servers] 17 | #${NETWORK_VELOCITY_SERVERS} 18 | 19 | try = [ 20 | "lobby" 21 | ] 22 | 23 | [forced-hosts] 24 | 25 | [advanced] 26 | # How large a Minecraft packet has to be before we compress it. Setting this to zero will 27 | # compress all packets, and setting it to -1 will disable compression entirely. 28 | compression-threshold = 256 29 | 30 | # How much compression should be done (from 0-9). The default is -1, which uses the 31 | # default level of 6. 32 | compression-level = -1 33 | 34 | # How fast (in milliseconds) are clients allowed to connect after the last connection? By 35 | # default, this is three seconds. Disable this by setting this to 0. 36 | login-ratelimit = 3000 37 | 38 | # Specify a custom timeout for connection timeouts here. The default is five seconds. 39 | connection-timeout = 5000 40 | 41 | # Specify a read timeout for connections here. The default is 30 seconds. 42 | read-timeout = 30000 43 | 44 | # Enables compatibility with HAProxy's PROXY protocol. If you don't know what this is for, then 45 | # don't enable it. 46 | haproxy-protocol = false 47 | 48 | # Enables TCP fast open support on the proxy. Requires the proxy to run on Linux. 49 | tcp-fast-open = false 50 | 51 | # Enables BungeeCord plugin messaging channel support on Velocity. 52 | bungee-plugin-message-channel = true 53 | 54 | # Shows ping requests to the proxy from clients. 55 | show-ping-requests = false 56 | 57 | # By default, Velocity will attempt to gracefully handle situations where the user unexpectedly 58 | # loses connection to the server without an explicit disconnect message by attempting to fall the 59 | # user back, except in the case of read timeouts. BungeeCord will disconnect the user instead. You 60 | # can disable this setting to use the BungeeCord behavior. 61 | failover-on-unexpected-server-disconnect = true 62 | 63 | # Declares the proxy commands to 1.13+ clients. 64 | announce-proxy-commands = true 65 | 66 | # Enables the logging of commands 67 | log-command-executions = false 68 | 69 | # Enables logging of player connections when connecting to the proxy, switching servers 70 | # and disconnecting from the proxy. 71 | log-player-connections = true 72 | 73 | [query] 74 | # Whether to enable responding to GameSpy 4 query responses or not. 75 | enabled = false 76 | 77 | # If query is enabled, on what port should the query protocol listen on? 78 | port = 25577 79 | 80 | # This is the map name that is reported to the query services. 81 | map = "Velocity" 82 | 83 | # Whether plugins should be shown in query response by default or not 84 | show-plugins = false 85 | -------------------------------------------------------------------------------- /examples/network/servers/proxy/server.toml: -------------------------------------------------------------------------------- 1 | name = "proxy" 2 | mc_version = "latest" 3 | 4 | [jar] 5 | type = "velocity" 6 | 7 | [variables] 8 | PORT = "25565" 9 | 10 | [launcher] 11 | eula_args = true 12 | nogui = true 13 | preset_flags = "proxy" 14 | 15 | [markdown] 16 | files = ["README.md"] 17 | auto_update = false 18 | 19 | [options] 20 | upload_to_mclogs = false 21 | -------------------------------------------------------------------------------- /examples/quilt/README.md: -------------------------------------------------------------------------------- 1 | # quilt 2 | 3 | [![mcman badge](https://img.shields.io/badge/uses-mcman-purple?logo=github)](https://github.com/ParadigmMC/mcman) 4 | 5 | 6 | 7 | 8 | | Version | Type | Loader | 9 | | ------- | ----------------------------- | -------- | 10 | | 1.19.2 | [Quilt](https://quiltmc.org/) | *Latest* | 11 | 12 | 13 | ## Mods 14 | 15 | 16 | | Name | Description | Version | 17 | | ------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------- | ----------------- | 18 | | [Create Fabric](https://modrinth.com/mod/create-fabric) | Building Tools and Aesthetic Technology | wKEEi1qX | 19 | | [Quilted Fabric API (QFAPI) / Quilt Standard Libraries (QSL)](https://modrinth.com/mod/qsl) | The standard libraries of the Quilt ecosystem. Essential for your modding experience on Quilt! | BTCxVi75 | 20 | | [spark](https://modrinth.com/mod/spark) | spark is a performance profiler for Minecraft clients, servers and proxies. | XhFbpH8f | 21 | | [LuckPerms](https://ci.lucko.me/job/LuckPerms) | The official download page for the latest LuckPerms builds can be found [here](https://luckperms.net/download). | latest / `Fabric` | 22 | 23 | -------------------------------------------------------------------------------- /examples/quilt/config/server.properties: -------------------------------------------------------------------------------- 1 | server-port=${PORT:25565} 2 | motd=${SERVER_NAME:Example mcman server with create} 3 | -------------------------------------------------------------------------------- /examples/quilt/hotreload.toml: -------------------------------------------------------------------------------- 1 | [[files]] 2 | path = "server.properties" 3 | action = "reload" 4 | -------------------------------------------------------------------------------- /examples/quilt/pack/index.toml: -------------------------------------------------------------------------------- 1 | hash-format = "sha256" 2 | 3 | [[files]] 4 | file = "mods/create-fabric-0.5.1-b-build.1089+mc1.19.2.pw.toml" 5 | hash = "ae51e9c4c9a18d10148cd9c433e97c86e7ebc551cfaeb1c411233a6faa58a8e3" 6 | metafile = true 7 | preserve = false 8 | 9 | [[files]] 10 | file = "mods/qfapi-4.0.0-beta.30_qsl-3.0.0-beta.29_fapi-0.76.0_mc-1.19.2.pw.toml" 11 | hash = "59eec7d8968f3ca5642dc4b8c200cc616b238dbb0bb20c9dba90d1fc7c511d80" 12 | metafile = true 13 | preserve = false 14 | 15 | [[files]] 16 | file = "mods/spark-1.10.37-fabric.pw.toml" 17 | hash = "ee035f74af6356f42182ac59c8078db57864c8bf498692735e880696e3b83519" 18 | metafile = true 19 | preserve = false 20 | 21 | [[files]] 22 | file = "mods/LuckPerms-Fabric-5.4.113.pw.toml" 23 | hash = "95464bb5ee7517c30832cfd4a7c9df86809d1430aac16e283e012a092270d6da" 24 | metafile = true 25 | preserve = false 26 | 27 | [[files]] 28 | file = "server.properties" 29 | hash = "995c65ecc7b74194662077f62c59e380a642b06de13b00b4e0160e83805c57d8" 30 | metafile = false 31 | preserve = false 32 | -------------------------------------------------------------------------------- /examples/quilt/pack/mods/LuckPerms-Fabric-5.4.111.pw.toml: -------------------------------------------------------------------------------- 1 | name = "LuckPerms-Fabric-5.4.111" 2 | filename = "LuckPerms-Fabric-5.4.111.jar" 3 | side = "both" 4 | 5 | [download] 6 | url = "https://ci.lucko.me/job/LuckPerms/1524/artifact/fabric/build/libs/LuckPerms-Fabric-5.4.111.jar" 7 | hash = "sha256" 8 | hash-format = "md5" 9 | mode = "url" 10 | 11 | [option] 12 | optional = false 13 | default = false 14 | -------------------------------------------------------------------------------- /examples/quilt/pack/mods/LuckPerms-Fabric-5.4.113.pw.toml: -------------------------------------------------------------------------------- 1 | name = "LuckPerms-Fabric-5.4.113" 2 | filename = "LuckPerms-Fabric-5.4.113.jar" 3 | side = "both" 4 | 5 | [download] 6 | url = "https://ci.lucko.me/job/LuckPerms/1526/artifact/fabric/build/libs/LuckPerms-Fabric-5.4.113.jar" 7 | hash = "sha256" 8 | hash-format = "md5" 9 | mode = "url" 10 | 11 | [option] 12 | optional = false 13 | default = false 14 | -------------------------------------------------------------------------------- /examples/quilt/pack/mods/create-fabric-0.5.1-b-build.1089+mc1.19.2.pw.toml: -------------------------------------------------------------------------------- 1 | name = "create-fabric-0.5.1-b-build.1089+mc1.19.2" 2 | filename = "create-fabric-0.5.1-b-build.1089+mc1.19.2.jar" 3 | side = "both" 4 | 5 | [download] 6 | url = "https://cdn.modrinth.com/data/Xbc0uyRg/versions/wKEEi1qX/create-fabric-0.5.1-b-build.1089%2Bmc1.19.2.jar" 7 | hash = "sha512" 8 | hash-format = "md5" 9 | mode = "url" 10 | 11 | [option] 12 | optional = false 13 | default = false 14 | 15 | [update.modrinth] 16 | mod-id = "create-fabric" 17 | version = "wKEEi1qX" 18 | -------------------------------------------------------------------------------- /examples/quilt/pack/mods/qfapi-4.0.0-beta.30_qsl-3.0.0-beta.29_fapi-0.76.0_mc-1.19.2.pw.toml: -------------------------------------------------------------------------------- 1 | name = "qfapi-4.0.0-beta.30_qsl-3.0.0-beta.29_fapi-0.76.0_mc-1.19.2" 2 | filename = "qfapi-4.0.0-beta.30_qsl-3.0.0-beta.29_fapi-0.76.0_mc-1.19.2.jar" 3 | side = "both" 4 | 5 | [download] 6 | url = "https://cdn.modrinth.com/data/qvIfYCYJ/versions/BTCxVi75/qfapi-4.0.0-beta.30_qsl-3.0.0-beta.29_fapi-0.76.0_mc-1.19.2.jar" 7 | hash = "sha512" 8 | hash-format = "md5" 9 | mode = "url" 10 | 11 | [option] 12 | optional = false 13 | default = false 14 | 15 | [update.modrinth] 16 | mod-id = "qsl" 17 | version = "BTCxVi75" 18 | -------------------------------------------------------------------------------- /examples/quilt/pack/mods/spark-1.10.37-fabric.pw.toml: -------------------------------------------------------------------------------- 1 | name = "spark-1.10.37-fabric" 2 | filename = "spark-1.10.37-fabric.jar" 3 | side = "both" 4 | 5 | [download] 6 | url = "https://cdn.modrinth.com/data/l6YH9Als/versions/XhFbpH8f/spark-1.10.37-fabric.jar" 7 | hash = "sha512" 8 | hash-format = "md5" 9 | mode = "url" 10 | 11 | [option] 12 | optional = false 13 | default = false 14 | 15 | [update.modrinth] 16 | mod-id = "spark" 17 | version = "XhFbpH8f" 18 | -------------------------------------------------------------------------------- /examples/quilt/pack/pack.toml: -------------------------------------------------------------------------------- 1 | name = "quilt" 2 | pack-format = "packwiz:1.1.0" 3 | 4 | [index] 5 | file = "index.toml" 6 | hash = "585200f562c18cb2d658b84f5ef816bbd3d6058905845a71cedf2c008e999bd0" 7 | hash-format = "sha256" 8 | metafile = false 9 | preserve = false 10 | 11 | [versions] 12 | minecraft = "1.19.2" 13 | quilt = "latest" 14 | -------------------------------------------------------------------------------- /examples/quilt/pack/server.properties: -------------------------------------------------------------------------------- 1 | server-port=${PORT:25565} 2 | motd=${SERVER_NAME:Example mcman server with create} 3 | -------------------------------------------------------------------------------- /examples/quilt/server.toml: -------------------------------------------------------------------------------- 1 | name = "quilt" 2 | mc_version = "1.19.2" 3 | 4 | [jar] 5 | type = "quilt" 6 | loader = "latest" 7 | installer = "latest" 8 | 9 | [variables] 10 | PORT = "25565" 11 | 12 | [launcher] 13 | eula_args = true 14 | nogui = true 15 | 16 | [[mods]] 17 | type = "modrinth" 18 | id = "create-fabric" 19 | version = "wKEEi1qX" 20 | 21 | [[mods]] 22 | type = "modrinth" 23 | id = "qsl" 24 | version = "BTCxVi75" 25 | 26 | [[mods]] 27 | type = "modrinth" 28 | id = "spark" 29 | version = "XhFbpH8f" 30 | 31 | [[mods]] 32 | type = "jenkins" 33 | url = "https://ci.lucko.me" 34 | job = "LuckPerms" 35 | build = "latest" 36 | artifact = "Fabric" 37 | 38 | [markdown] 39 | files = ["README.md"] 40 | auto_update = true 41 | 42 | [options] 43 | upload_to_mclogs = true 44 | -------------------------------------------------------------------------------- /examples/spigot/README.md: -------------------------------------------------------------------------------- 1 | # spigot 2 | 3 | [![mcman badge](https://img.shields.io/badge/uses-mcman-purple?logo=github)](https://github.com/ParadigmMC/mcman) 4 | 5 | 6 | 7 | 8 | 9 | 10 | ## Plugins 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /examples/spigot/config/server.properties: -------------------------------------------------------------------------------- 1 | server-port=${PORT:25565} 2 | motd=${SERVER_NAME:A Minecraft Server} 3 | -------------------------------------------------------------------------------- /examples/spigot/server.toml: -------------------------------------------------------------------------------- 1 | name = "spigot" 2 | mc_version = "1.20.2" 3 | 4 | [jar] 5 | type = "buildtools" 6 | software = "spigot" 7 | 8 | [variables] 9 | PORT = "25565" 10 | 11 | [launcher] 12 | eula_args = true 13 | nogui = true 14 | preset_flags = "aikars" 15 | 16 | [options] 17 | upload_to_mclogs = false 18 | -------------------------------------------------------------------------------- /examples/vanilla/README.md: -------------------------------------------------------------------------------- 1 | # vanilla-example 2 | 3 | [![mcman badge](https://img.shields.io/badge/uses-mcman-purple?logo=github)](https://github.com/ParadigmMC/mcman) 4 | 5 | An example vanilla server with mcman. 6 | 7 | 8 | | Version | Type | 9 | | ------- | ------- | 10 | | 1.20.1 | Vanilla | 11 | 12 | -------------------------------------------------------------------------------- /examples/vanilla/config/server.properties: -------------------------------------------------------------------------------- 1 | server-port=${PORT:25565} 2 | motd=${SERVER_NAME:A Minecraft Server} 3 | -------------------------------------------------------------------------------- /examples/vanilla/server.toml: -------------------------------------------------------------------------------- 1 | name = "vanilla-example" 2 | mc_version = "1.20.1" 3 | 4 | [jar] 5 | type = "vanilla" 6 | 7 | [variables] 8 | PORT = "25565" 9 | 10 | [launcher] 11 | aikars_flags = true 12 | proxy_flags = false 13 | nogui = true 14 | eula_args = true 15 | 16 | [markdown] 17 | files = ["README.md"] 18 | auto_update = false 19 | -------------------------------------------------------------------------------- /flake.lock: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": { 3 | "flake-utils": { 4 | "inputs": { 5 | "systems": "systems" 6 | }, 7 | "locked": { 8 | "lastModified": 1701680307, 9 | "narHash": "sha256-kAuep2h5ajznlPMD9rnQyffWG8EM/C73lejGofXvdM8=", 10 | "owner": "numtide", 11 | "repo": "flake-utils", 12 | "rev": "4022d587cbbfd70fe950c1e2083a02621806a725", 13 | "type": "github" 14 | }, 15 | "original": { 16 | "owner": "numtide", 17 | "repo": "flake-utils", 18 | "type": "github" 19 | } 20 | }, 21 | "gitignore": { 22 | "flake": false, 23 | "locked": { 24 | "lastModified": 1703887061, 25 | "narHash": "sha256-gGPa9qWNc6eCXT/+Z5/zMkyYOuRZqeFZBDbopNZQkuY=", 26 | "owner": "hercules-ci", 27 | "repo": "gitignore.nix", 28 | "rev": "43e1aa1308018f37118e34d3a9cb4f5e75dc11d5", 29 | "type": "github" 30 | }, 31 | "original": { 32 | "owner": "hercules-ci", 33 | "repo": "gitignore.nix", 34 | "type": "github" 35 | } 36 | }, 37 | "nixpkgs": { 38 | "locked": { 39 | "lastModified": 1703992652, 40 | "narHash": "sha256-C0o8AUyu8xYgJ36kOxJfXIroy9if/G6aJbNOpA5W0+M=", 41 | "owner": "nixos", 42 | "repo": "nixpkgs", 43 | "rev": "32f63574c85fbc80e4ba1fbb932cde9619bad25e", 44 | "type": "github" 45 | }, 46 | "original": { 47 | "owner": "nixos", 48 | "ref": "nixos-23.11", 49 | "repo": "nixpkgs", 50 | "type": "github" 51 | } 52 | }, 53 | "root": { 54 | "inputs": { 55 | "flake-utils": "flake-utils", 56 | "gitignore": "gitignore", 57 | "nixpkgs": "nixpkgs" 58 | } 59 | }, 60 | "systems": { 61 | "locked": { 62 | "lastModified": 1681028828, 63 | "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", 64 | "owner": "nix-systems", 65 | "repo": "default", 66 | "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", 67 | "type": "github" 68 | }, 69 | "original": { 70 | "owner": "nix-systems", 71 | "repo": "default", 72 | "type": "github" 73 | } 74 | } 75 | }, 76 | "root": "root", 77 | "version": 7 78 | } 79 | -------------------------------------------------------------------------------- /flake.nix: -------------------------------------------------------------------------------- 1 | { 2 | description = "Powerful Minecraft Server Manager CLI"; 3 | 4 | inputs = { 5 | nixpkgs.url = "github:nixos/nixpkgs/nixos-23.11"; 6 | flake-utils.url = "github:numtide/flake-utils"; 7 | gitignore = { url = "github:hercules-ci/gitignore.nix"; flake = false; }; 8 | }; 9 | 10 | outputs = { self, nixpkgs, flake-utils, ... }: 11 | flake-utils.lib.eachDefaultSystem (system: 12 | let 13 | pkgs = nixpkgs.legacyPackages.${system}; 14 | cargoToml = builtins.fromTOML (builtins.readFile ./Cargo.toml); 15 | in rec { 16 | legacyPackages.mcman = pkgs.rustPlatform.buildRustPackage { 17 | inherit (cargoToml.package) name version; 18 | src = ./.; 19 | cargoLock.lockFile = ./Cargo.lock; 20 | cargoLock.outputHashes = { 21 | "mcapi-0.2.0" = "sha256-wHXA+4DQVQpfSCfJLFuc9kUSwyqo6T4o0PypYdhpp5s="; 22 | "pathdiff-0.2.1" = "sha256-+X1afTOLIVk1AOopQwnjdobKw6P8BXEXkdZOieHW5Os="; 23 | "rpackwiz-0.1.0" = "sha256-pOotNPIZS/BXiJWZVECXzP1lkb/o9J1tu6G2OqyEnI8="; 24 | }; 25 | }; 26 | defaultPackage = legacyPackages.mcman; 27 | } 28 | ); 29 | } 30 | -------------------------------------------------------------------------------- /hotreload.schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-07/schema#", 3 | "title": "hotreload.toml", 4 | "description": "mcman hotreload.toml schema", 5 | "type": "object", 6 | "properties": { 7 | "files": { 8 | "type": "array", 9 | "items": { 10 | "type": "object", 11 | "properties": { 12 | "path": { "type": "string" }, 13 | "action": { 14 | "oneOf": [ 15 | { 16 | "const": "reload" 17 | }, 18 | { 19 | "const": "restart" 20 | }, 21 | { 22 | "type": "string", 23 | "pattern": "/.*" 24 | } 25 | ] 26 | } 27 | } 28 | } 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /mkdocs.yml: -------------------------------------------------------------------------------- 1 | site_name: mcman 2 | repo_url: https://github.com/ParadigmMC/mcman 3 | repo_name: ParadigmMC/mcman 4 | nav: 5 | - Home: index.md 6 | - Installation: installation.md 7 | - Concepts: 8 | - concepts/getting-started.md 9 | - concepts/importing-modpacks.md 10 | - concepts/building.md 11 | - concepts/variables.md 12 | - concepts/dev.md 13 | - concepts/using-worlds.md 14 | - concepts/network.md 15 | - concepts/caching.md 16 | - concepts/options.md 17 | - Commands: 18 | - commands/index.md 19 | - init: commands/init.md 20 | - build: commands/build.md 21 | - run: commands/run.md 22 | - dev: commands/dev.md 23 | - cache: commands/cache.md 24 | - world pack|unpack: commands/world.md 25 | - import url|mrpack|packwiz: commands/import.md 26 | - export mrpack|packwiz: commands/export.md 27 | - info: commands/info.md 28 | - pull: commands/pull.md 29 | - markdown: commands/markdown.md 30 | - env: commands/env.md 31 | - version: commands/version.md 32 | - Reference: 33 | - server.toml: 34 | - reference/server.toml.md 35 | - reference/world.md 36 | - reference/markdown-options.md 37 | - reference/server-launcher.md 38 | - reference/clientsidemod.md 39 | - network.toml: reference/network.toml.md 40 | - Hook: reference/hook.md 41 | - Server Type: 42 | - reference/servertype/index.md 43 | - reference/servertype/vanilla.md 44 | - reference/servertype/fabric.md 45 | - reference/servertype/quilt.md 46 | - reference/servertype/neoforge.md 47 | - reference/servertype/forge.md 48 | - reference/servertype/papermc.md 49 | - reference/servertype/purpurmc.md 50 | - reference/servertype/bungeecord.md 51 | - reference/servertype/buildtools.md 52 | - Downloadable: 53 | - reference/downloadable/index.md 54 | - reference/downloadable/modrinth.md 55 | - reference/downloadable/curserinth.md 56 | - reference/downloadable/hangar.md 57 | - reference/downloadable/spigot.md 58 | - reference/downloadable/github-releases.md 59 | - reference/downloadable/jenkins.md 60 | - reference/downloadable/maven.md 61 | - reference/downloadable/custom-url.md 62 | - Lockfile: reference/lockfile.md 63 | theme: 64 | name: material 65 | icon: 66 | repo: fontawesome/brands/github 67 | logo: material/server 68 | palette: 69 | scheme: slate 70 | primary: deep purple 71 | accent: purple 72 | features: 73 | - navigation.instant 74 | - navigation.tracking 75 | - navigation.tabs 76 | - navigation.path 77 | - content.code.copy 78 | - content.code.annotate 79 | markdown_extensions: 80 | - admonition 81 | - pymdownx.details 82 | - pymdownx.highlight: 83 | anchor_linenums: true 84 | - pymdownx.inlinehilite 85 | - pymdownx.snippets 86 | - pymdownx.superfences 87 | - def_list 88 | - attr_list 89 | - md_in_html 90 | - pymdownx.emoji: 91 | emoji_index: !!python/name:materialx.emoji.twemoji 92 | emoji_generator: !!python/name:materialx.emoji.to_svg 93 | - pymdownx.tabbed: 94 | alternate_style: true 95 | extra: 96 | social: 97 | - icon: fontawesome/brands/github 98 | link: https://github.com/ParadigmMC/mcman -------------------------------------------------------------------------------- /network.schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-07/schema#", 3 | "title": "network.toml schema", 4 | "description": "Schema for mcman's network.toml file", 5 | "type": "object", 6 | "properties": { 7 | "name": { 8 | "description": "Name of the network", 9 | "type": "string" 10 | }, 11 | "proxy": { 12 | "description": "The proxy server's name", 13 | "type": "string" 14 | }, 15 | "port": { 16 | "type": "integer", 17 | "default": 25565 18 | }, 19 | "servers": { 20 | "description": "Table of servers", 21 | "type": "object", 22 | "additionalProperties": { 23 | "port": { 24 | "type": "integer", 25 | "default": 25565 26 | }, 27 | "ip_address": { 28 | "description": "Optional server ip address", 29 | "type": "string" 30 | } 31 | } 32 | }, 33 | "variables": { 34 | "description": "Variables shared between all servers", 35 | "type": "object" 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /res/aikars_flags: -------------------------------------------------------------------------------- 1 | --add-modules=jdk.incubator.vector -XX:+UseG1GC -XX:+ParallelRefProcEnabled -XX:MaxGCPauseMillis=200 -XX:+UnlockExperimentalVMOptions -XX:+DisableExplicitGC -XX:+AlwaysPreTouch -XX:G1HeapWastePercent=5 -XX:G1MixedGCCountTarget=4 -XX:InitiatingHeapOccupancyPercent=15 -XX:G1MixedGCLiveThresholdPercent=90 -XX:G1RSetUpdatingPauseTimePercent=5 -XX:SurvivorRatio=32 -XX:+PerfDisableSharedMem -XX:MaxTenuringThreshold=1 -Dusing.aikars.flags=https://mcflags.emc.gs -Daikars.new.flags=true -XX:G1NewSizePercent=30 -XX:G1MaxNewSizePercent=40 -XX:G1HeapRegionSize=8M -XX:G1ReservePercent=20 -------------------------------------------------------------------------------- /res/default_dockerfile: -------------------------------------------------------------------------------- 1 | FROM ghcr.io/paradigmmc/mcman:latest as builder 2 | WORKDIR /server 3 | COPY . . 4 | RUN mcman build 5 | 6 | FROM eclipse-temurin:21-alpine 7 | USER 1000:1000 8 | WORKDIR /server 9 | COPY --from=builder --chown=1000:1000 /server/server/ /server 10 | ENTRYPOINT [ "/server/start.sh" ] 11 | -------------------------------------------------------------------------------- /res/default_dockerignore: -------------------------------------------------------------------------------- 1 | .dockerignore 2 | .git 3 | .gitignore 4 | Dockerfile 5 | server/ 6 | -------------------------------------------------------------------------------- /res/default_readme: -------------------------------------------------------------------------------- 1 | # {SERVER_NAME} 2 | 3 | [![mcman badge](https://img.shields.io/badge/uses-mcman-purple?logo=github)](https://github.com/ParadigmMC/mcman) 4 | 5 | 6 | 7 | 8 | 9 | 10 | ## {ADDON_HEADER} 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /res/default_readme_network: -------------------------------------------------------------------------------- 1 | # {NETWORK_NAME} 2 | 3 | [![mcman badge](https://img.shields.io/badge/uses-mcman-purple?logo=github)](https://github.com/ParadigmMC/mcman) 4 | 5 | 6 | 7 | ## Servers 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /res/markdown_links: -------------------------------------------------------------------------------- 1 | [mcman repo]: https://github.com/ParadigmMC/mcman 2 | [mcman badge]: https://img.shields.io/badge/uses-mcman-purple?logo=github 3 | [github logo]: https://github.githubassets.com/images/modules/logos_page/GitHub-Mark.png -------------------------------------------------------------------------------- /res/proxy_flags: -------------------------------------------------------------------------------- 1 | -XX:+UseG1GC -XX:G1HeapRegionSize=4M -XX:+UnlockExperimentalVMOptions -XX:+ParallelRefProcEnabled -XX:+AlwaysPreTouch -XX:MaxInlineLevel=15 -------------------------------------------------------------------------------- /res/server.properties: -------------------------------------------------------------------------------- 1 | server-port=${SERVER_PORT} 2 | motd=${SERVER_NAME} made with mcman! 3 | -------------------------------------------------------------------------------- /res/workflows/packwiz.yml: -------------------------------------------------------------------------------- 1 | name: Export Packwiz 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | jobs: 9 | packwiz-export: 10 | runs-on: ubuntu-latest 11 | 12 | permissions: 13 | contents: write 14 | 15 | steps: 16 | - uses: actions/checkout@v2 17 | 18 | - name: Install mcman 19 | run: | 20 | sudo curl -L -o /usr/bin/mcman https://github.com/ParadigmMC/mcman/releases/latest/download/mcman 21 | sudo chmod +x /usr/bin/mcman 22 | 23 | - name: Run mcman export packwiz 24 | run: mcman export packwiz 25 | env: 26 | MODPACK_VERSION: ${{ github.sha }} 27 | 28 | - uses: stefanzweifel/git-auto-commit-action@v5 29 | with: 30 | commit_message: Update packwiz pack 31 | -------------------------------------------------------------------------------- /res/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test server 2 | 3 | on: 4 | push: 5 | branches: main 6 | 7 | jobs: 8 | test: 9 | env: 10 | upload_to_mclogs: true 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Checkout 14 | uses: actions/checkout@v3 15 | - name: Setup java 16 | uses: actions/setup-java@v3 17 | with: 18 | distribution: 'temurin' 19 | java-version: '17' 20 | - name: Cache 21 | id: cache 22 | uses: actions/cache@v3 23 | with: 24 | key: mcman-build-${{ hashFiles('server.toml') }} 25 | path: | 26 | ./server 27 | ~/.cache/mcman 28 | restore-keys: | 29 | mcman-build-${{ hashFiles('server.toml') }} 30 | mcman-build- 31 | mcman- 32 | - name: Install mcman 33 | run: | 34 | sudo curl -L -o /usr/bin/mcman https://github.com/ParadigmMC/mcman/releases/latest/download/mcman 35 | sudo chmod +x /usr/bin/mcman 36 | - name: Test the server 37 | id: test 38 | run: | 39 | mcman run --test 40 | - name: Archive log 41 | uses: actions/upload-artifact@v3 42 | with: 43 | name: latest.log 44 | path: | 45 | server/logs/latest.log 46 | - name: Archive crash reports 47 | uses: actions/upload-artifact@v3 48 | if: steps.test.outcome == 'failure' 49 | with: 50 | name: crash 51 | path: | 52 | server/crash-reports/* 53 | -------------------------------------------------------------------------------- /src/app/caching.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | fs::File, 3 | io::{BufReader, BufWriter}, 4 | path::PathBuf, 5 | }; 6 | 7 | use anyhow::{Context, Result}; 8 | use serde::de::DeserializeOwned; 9 | 10 | pub struct Cache(pub PathBuf); 11 | 12 | impl Cache { 13 | pub fn cache_root() -> Option { 14 | Some(dirs::cache_dir()?.join("mcman")) 15 | } 16 | 17 | pub fn get_cache(namespace: &str) -> Option { 18 | let dir = Self::cache_root()?.join(namespace); 19 | Some(Self(dir)) 20 | } 21 | 22 | pub fn get_json(&self, path: &str) -> Result { 23 | let file = File::open(self.0.join(path))?; 24 | let reader = BufReader::new(file); 25 | 26 | Ok(serde_json::from_reader(reader)?) 27 | } 28 | 29 | pub fn path(&self, path: &str) -> PathBuf { 30 | self.0.join(path) 31 | } 32 | 33 | pub fn exists(&self, path: &str) -> bool { 34 | self.path(path).exists() 35 | } 36 | 37 | pub fn try_get_json(&self, path: &str) -> Result> { 38 | Ok(if self.exists(path) { 39 | Some(self.get_json(path)?) 40 | } else { 41 | None 42 | }) 43 | } 44 | 45 | pub fn write_json(&self, path: &str, data: &T) -> Result<()> { 46 | std::fs::create_dir_all(self.path(path).parent().unwrap())?; 47 | let writer = BufWriter::new( 48 | File::create(self.path(path)).context(format!("Creating cache file at: {path}"))?, 49 | ); 50 | 51 | Ok(serde_json::to_writer(writer, data)?) 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/app/hashing.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{Context, Result}; 2 | use digest::{Digest, DynDigest}; 3 | use indicatif::ProgressBar; 4 | use sha2::Sha256; 5 | use std::{collections::HashMap, marker::Unpin, path::PathBuf}; 6 | use tokio::{ 7 | fs::File, 8 | io::{AsyncRead, AsyncWrite}, 9 | }; 10 | use tokio_stream::StreamExt; 11 | use tokio_util::io::ReaderStream; 12 | 13 | use super::{App, ResolvedFile}; 14 | 15 | impl App { 16 | pub fn get_best_hash(hashes: &HashMap) -> Option<(String, String)> { 17 | hashes 18 | .get_key_value("sha512") 19 | .or(hashes.get_key_value("sha256")) 20 | .or(hashes.get_key_value("md5")) 21 | .or(hashes.get_key_value("sha1")) 22 | .map(|(k, v)| (k.clone(), v.clone())) 23 | } 24 | 25 | pub async fn hash_resolved_file(&self, resolved: &ResolvedFile) -> Result<(String, String)> { 26 | if let Some(pair) = Self::get_best_hash(&resolved.hashes) { 27 | Ok(pair) 28 | } else { 29 | // calculate hash manually 30 | 31 | let (file_path, is_temp) = 32 | if let Some((path, true)) = self.resolve_cached_file(&resolved.cache) { 33 | // file exists in cache dir 34 | (path, false) 35 | } else { 36 | // either can't cache or isnt in cache dir 37 | self.download_resolved( 38 | resolved.clone(), 39 | PathBuf::from("."), 40 | ProgressBar::new_spinner(), 41 | ) 42 | .await?; 43 | (PathBuf::from(".").join(&resolved.filename), true) 44 | }; 45 | 46 | let preferred_hash = "sha256"; 47 | let mut digester = Self::create_hasher(preferred_hash); 48 | 49 | let pb = self.multi_progress.add( 50 | ProgressBar::new_spinner() 51 | .with_message(format!("Calculating hash for {}", resolved.filename)), 52 | ); 53 | 54 | let file = File::open(&file_path) 55 | .await 56 | .context(format!("Opening file '{}'", file_path.display()))?; 57 | 58 | let mut stream = ReaderStream::new(file); 59 | while let Some(item) = stream.next().await { 60 | let item = item?; 61 | digester.update(&item); 62 | } 63 | 64 | if is_temp { 65 | pb.set_message("Cleaning up..."); 66 | 67 | tokio::fs::remove_file(&file_path) 68 | .await 69 | .context(format!("Deleting {}", file_path.display()))?; 70 | } 71 | 72 | pb.finish_and_clear(); 73 | 74 | let stream_hash = hex::encode(&digester.finalize()); 75 | 76 | Ok((preferred_hash.to_owned(), stream_hash)) 77 | } 78 | } 79 | 80 | pub async fn copy_with_hashing( 81 | source: &mut R, 82 | dest: &mut W, 83 | mut digester: Box, 84 | ) -> Result { 85 | let mut stream = ReaderStream::new(source); 86 | while let Some(item) = stream.next().await { 87 | let item = item?; 88 | 89 | digester.update(&item); 90 | 91 | tokio::io::copy(&mut item.as_ref(), dest).await?; 92 | } 93 | 94 | Ok(hex::encode(&digester.finalize())) 95 | } 96 | 97 | pub fn hash_sha256(contents: &str) -> String { 98 | let mut hasher = Sha256::new(); 99 | 100 | Digest::update(&mut hasher, contents); 101 | 102 | hex::encode(hasher.finalize()) 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /src/app/progress.rs: -------------------------------------------------------------------------------- 1 | use indicatif::ProgressBar; 2 | 3 | pub trait ProgressBarExt {} 4 | 5 | impl ProgressBarExt for ProgressBar {} 6 | -------------------------------------------------------------------------------- /src/app/resolvable.rs: -------------------------------------------------------------------------------- 1 | use std::{borrow::Cow, collections::HashMap}; 2 | 3 | use anyhow::Result; 4 | use serde::{Deserialize, Serialize}; 5 | 6 | use super::App; 7 | 8 | pub trait Resolvable { 9 | async fn resolve_source(&self, app: &App) -> Result; 10 | } 11 | 12 | #[derive(Debug, Clone, Serialize, Deserialize)] 13 | pub struct ResolvedFile { 14 | pub url: String, 15 | pub filename: String, 16 | pub cache: CacheStrategy, 17 | pub size: Option, 18 | pub hashes: HashMap, 19 | } 20 | 21 | #[derive(Debug, Clone, Serialize, Deserialize, Default)] 22 | #[serde(tag = "type")] 23 | pub enum CacheStrategy { 24 | File { 25 | namespace: Cow<'static, str>, 26 | path: String, 27 | }, 28 | Indexed { 29 | index_path: String, 30 | key: String, 31 | value: String, 32 | }, 33 | #[default] 34 | None, 35 | } 36 | -------------------------------------------------------------------------------- /src/app/steps.rs: -------------------------------------------------------------------------------- 1 | use std::path::PathBuf; 2 | 3 | use anyhow::Result; 4 | use indicatif::ProgressBar; 5 | 6 | use super::{ResolvedFile, App}; 7 | 8 | #[derive(Debug, Clone)] 9 | pub enum Step { 10 | Download(DownloadTask), 11 | ExecuteJava { 12 | jar: String, 13 | args: Vec, 14 | path: PathBuf, 15 | } 16 | } 17 | 18 | #[derive(Debug, Clone)] 19 | pub struct DownloadTask { 20 | pub resolved_file: ResolvedFile, 21 | pub path: PathBuf, 22 | } 23 | 24 | impl App { 25 | pub async fn run_step(&self, step: &Step) -> Result<()> { 26 | match step { 27 | Step::Download(folder, resolved_file) => { 28 | self.download_resolved( 29 | resolved_file.clone(), 30 | folder.clone(), 31 | self.multi_progress.add(ProgressBar::new_spinner()) 32 | ).await?; 33 | } 34 | 35 | Step::ExecuteJava { jar, args, path } => { 36 | todo!(); 37 | } 38 | } 39 | 40 | Ok(()) 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/commands/add/mod.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | 3 | use crate::app::App; 4 | 5 | mod modrinth; 6 | 7 | #[derive(clap::Subcommand)] 8 | pub enum Commands { 9 | /// Add from modrinth 10 | #[command(alias = "mr")] 11 | Modrinth(modrinth::Args), 12 | } 13 | 14 | pub async fn run(app: App, args: Commands) -> Result<()> { 15 | match args { 16 | Commands::Modrinth(args) => modrinth::run(app, args).await?, 17 | } 18 | Ok(()) 19 | } 20 | -------------------------------------------------------------------------------- /src/commands/add/modrinth.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{bail, Context, Result}; 2 | use std::borrow::Cow; 3 | 4 | use crate::{ 5 | app::{App, Prefix}, 6 | model::Downloadable, 7 | util::SelectItem, 8 | }; 9 | 10 | #[derive(clap::Args)] 11 | pub struct Args { 12 | search: Option, 13 | } 14 | 15 | pub async fn run(mut app: App, args: Args) -> Result<()> { 16 | let search_type = app.select( 17 | "Which project type?", 18 | &[ 19 | SelectItem("mod", Cow::Borrowed("Mods")), 20 | SelectItem("datapack", Cow::Borrowed("Datapacks")), 21 | SelectItem("modpack", Cow::Borrowed("Modpacks")), 22 | ], 23 | )?; 24 | 25 | let query = if let Some(s) = args.search { 26 | s.clone() 27 | } else { 28 | app.prompt_string("Search on Modrinth")? 29 | }; 30 | 31 | let projects = app 32 | .modrinth() 33 | .search(&query) 34 | .await 35 | .context("Searching modrinth")?; 36 | 37 | if projects.is_empty() { 38 | bail!("No modrinth projects found for query '{query}'"); 39 | } 40 | 41 | let items = projects 42 | .into_iter() 43 | .filter(|p| p.project_type == search_type) 44 | .map(|p| { 45 | SelectItem( 46 | p.clone(), 47 | Cow::Owned(format!( 48 | "{} [{}]\n{s:w$}{}", 49 | p.title, 50 | p.slug, 51 | p.description, 52 | s = " ", 53 | w = 4 54 | )), 55 | ) 56 | }) 57 | .collect::>(); 58 | 59 | let project = app.select("Which project?", &items)?; 60 | 61 | let versions = app 62 | .modrinth() 63 | .fetch_versions(&project.slug) 64 | .await 65 | .context("Fetching modrinth versions")?; 66 | 67 | let version = app.select( 68 | "Which version?", 69 | &versions 70 | .into_iter() 71 | .map(|v| { 72 | SelectItem( 73 | v.clone(), 74 | Cow::Owned(format!("[{}]: {}", v.version_number, v.name)), 75 | ) 76 | }) 77 | .collect::>(), 78 | )?; 79 | 80 | match if version.loaders.iter().any(|s| s.as_str() == "datapack") { 81 | if version.loaders.len() > 1 { 82 | app.select( 83 | "Import as...", 84 | &[ 85 | SelectItem("datapack", Cow::Borrowed("Datapack")), 86 | SelectItem("mod", Cow::Borrowed("Mod/Plugin")), 87 | ], 88 | )? 89 | } else { 90 | "datapack" 91 | } 92 | } else { 93 | project.project_type.as_str() 94 | } { 95 | "modpack" => { 96 | todo!("Modpack importing currently unsupported") 97 | } 98 | "mod" => { 99 | app.add_addon_inferred(Downloadable::Modrinth { 100 | id: project.slug.clone(), 101 | version: version.id.clone(), 102 | })?; 103 | 104 | app.save_changes()?; 105 | app.notify(Prefix::Imported, format!("{} from modrinth", project.title)); 106 | app.refresh_markdown().await?; 107 | } 108 | "datapack" => { 109 | app.add_datapack(Downloadable::Modrinth { 110 | id: project.slug.clone(), 111 | version: version.id.clone(), 112 | })?; 113 | 114 | app.save_changes()?; 115 | app.refresh_markdown().await?; 116 | } 117 | ty => bail!("Unsupported modrinth project type: '{ty}'"), 118 | } 119 | 120 | Ok(()) 121 | } 122 | -------------------------------------------------------------------------------- /src/commands/build.rs: -------------------------------------------------------------------------------- 1 | use std::path::PathBuf; 2 | 3 | use anyhow::{Context, Result}; 4 | 5 | use crate::{app::App, core::BuildContext, model::Lockfile}; 6 | 7 | #[derive(clap::Args)] 8 | pub struct BuildArgs { 9 | /// The output directory for the server 10 | #[arg(short, long, value_name = "file")] 11 | output: Option, 12 | /// Skip some stages 13 | #[arg(short, long, value_name = "stages")] 14 | skip: Vec, 15 | #[arg(long)] 16 | /// Don't skip downloading already downloaded jars 17 | force: bool, 18 | } 19 | 20 | impl BuildArgs { 21 | pub fn create_build_context(self, app: &mut App) -> Result> { 22 | let default_output = app.server.path.join("server"); 23 | let output_dir = self.output.unwrap_or(default_output); 24 | 25 | std::fs::create_dir_all(&output_dir).context("Failed to create output directory")?; 26 | 27 | Ok(BuildContext { 28 | app, 29 | force: self.force, 30 | skip_stages: self.skip, 31 | output_dir, 32 | lockfile: Lockfile::default(), 33 | new_lockfile: Lockfile::default(), 34 | server_process: None, 35 | }) 36 | } 37 | } 38 | 39 | pub async fn run(mut app: App, args: BuildArgs) -> Result<()> { 40 | let mut ctx = args.create_build_context(&mut app)?; 41 | 42 | ctx.build_all().await?; 43 | 44 | Ok(()) 45 | } 46 | -------------------------------------------------------------------------------- /src/commands/cache.rs: -------------------------------------------------------------------------------- 1 | use std::time::Duration; 2 | 3 | use anyhow::{bail, Context, Result}; 4 | use console::style; 5 | use indicatif::{ProgressBar, ProgressStyle}; 6 | 7 | use crate::app::Cache; 8 | use std::fs; 9 | 10 | #[derive(clap::Subcommand, Clone, Copy)] 11 | pub enum Commands { 12 | /// Print cache root 13 | Path, 14 | /// List caches 15 | List { 16 | /// Prints entries under namespaces 17 | #[arg(short)] 18 | detailed: bool, 19 | }, 20 | /// Open the cache folder 21 | Open, 22 | /// Delete everything from cache (no confirmation) 23 | Clear, 24 | } 25 | 26 | pub fn run(commands: Commands) -> Result<()> { 27 | match Cache::cache_root() { 28 | Some(cache_folder) => { 29 | match commands { 30 | Commands::Path => { 31 | println!("{}", cache_folder.to_string_lossy()); 32 | } 33 | 34 | Commands::List { detailed } => { 35 | println!( 36 | " Listing cache...\n {}", 37 | style(format!("Folder: {}", cache_folder.to_string_lossy())).dim() 38 | ); 39 | 40 | let mut namespaces = 0; 41 | let mut all = 0; 42 | for entry in fs::read_dir(&cache_folder)? { 43 | let entry = entry?; 44 | let count = fs::read_dir(entry.path())?.count(); 45 | println!( 46 | " {} {} {} {count} entries", 47 | style("=>").cyan(), 48 | style(entry.file_name().to_string_lossy()).green().bold(), 49 | style("-").dim() 50 | ); 51 | 52 | if detailed { 53 | for entry in fs::read_dir(entry.path())? { 54 | let entry = entry?; 55 | println!( 56 | " {} {}", 57 | style("└").green(), 58 | style(entry.file_name().to_string_lossy()).dim(), 59 | ); 60 | } 61 | } 62 | 63 | namespaces += 1; 64 | all += count; 65 | } 66 | 67 | println!(" {all} entries in {namespaces} namespaces in total"); 68 | } 69 | 70 | Commands::Open => { 71 | opener::open(cache_folder).context("Opening cache folder")?; 72 | } 73 | 74 | Commands::Clear => { 75 | let pb = ProgressBar::new_spinner() 76 | .with_style(ProgressStyle::with_template( 77 | "{spinner:.cyan.bold} {prefix:.green} {message}", 78 | )?) 79 | .with_prefix("Deleting"); 80 | pb.enable_steady_tick(Duration::from_millis(250)); 81 | 82 | for entry in fs::read_dir(&cache_folder)? { 83 | let entry = entry?; 84 | 85 | pb.set_message(entry.file_name().to_string_lossy().to_string()); 86 | 87 | fs::remove_dir_all(entry.path())?; 88 | } 89 | 90 | pb.finish_and_clear(); 91 | println!(" Cache has been cleared"); 92 | } 93 | } 94 | 95 | Ok(()) 96 | } 97 | 98 | None => bail!("Cache directory was missing, maybe it's disabled?"), 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /src/commands/completions.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use clap::{Args, Command, CommandFactory}; 3 | use clap_complete::{generate, Generator, Shell}; 4 | use std::io; 5 | 6 | use crate::Cli; 7 | 8 | /// Completion arguments. 9 | #[derive(Args)] 10 | pub struct CompletionArgs { 11 | /// Generate completions for the specified shell. 12 | pub shell: Shell, 13 | } 14 | 15 | fn print_completions(generator: G, cmd: &mut Command) { 16 | generate( 17 | generator, 18 | cmd, 19 | cmd.get_name().to_string(), 20 | &mut io::stdout(), 21 | ); 22 | } 23 | 24 | pub fn run(args: &CompletionArgs) -> Result<()> { 25 | print_completions(args.shell, &mut Cli::command()); 26 | std::process::exit(0); 27 | } 28 | -------------------------------------------------------------------------------- /src/commands/dedupe.rs: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ParadigmMC/mcman/00668083634c6d20b96dbc7c5f5dd21615ed244c/src/commands/dedupe.rs -------------------------------------------------------------------------------- /src/commands/dev.rs: -------------------------------------------------------------------------------- 1 | use std::sync::{Arc, Mutex}; 2 | 3 | use anyhow::Result; 4 | 5 | use crate::{ 6 | app::App, 7 | hot_reload::{config::HotReloadConfig, DevSession}, 8 | }; 9 | 10 | use super::run::RunArgs; 11 | 12 | #[derive(clap::Args)] 13 | pub struct DevArgs { 14 | #[command(flatten)] 15 | run_args: RunArgs, 16 | } 17 | 18 | impl DevArgs { 19 | pub fn create_dev_session(self, app: &mut App) -> Result> { 20 | let config_path = app.server.path.join("hotreload.toml"); 21 | 22 | let config = if config_path.exists() { 23 | HotReloadConfig::load_from(&config_path)? 24 | } else { 25 | app.info("Generated hotreload.toml"); 26 | 27 | let cfg = HotReloadConfig { 28 | path: config_path, 29 | ..Default::default() 30 | }; 31 | 32 | cfg.save()?; 33 | cfg 34 | }; 35 | 36 | let mut dev_session = self.run_args.create_dev_session(app)?; 37 | dev_session.hot_reload = Some(Arc::new(Mutex::new(config))); 38 | // no. 39 | dev_session.test_mode = false; 40 | 41 | Ok(dev_session) 42 | } 43 | } 44 | 45 | pub async fn run(mut app: App, args: DevArgs) -> Result<()> { 46 | let dev_session = args.create_dev_session(&mut app)?; 47 | dev_session.start().await?; 48 | 49 | println!(); 50 | 51 | Ok(()) 52 | } 53 | -------------------------------------------------------------------------------- /src/commands/download.rs: -------------------------------------------------------------------------------- 1 | use std::path::PathBuf; 2 | 3 | use anyhow::Result; 4 | use indicatif::ProgressBar; 5 | 6 | use crate::app::App; 7 | 8 | #[derive(clap::Args)] 9 | pub struct Args { 10 | /// A downloadable to download 11 | downloadable: Option, 12 | } 13 | 14 | pub async fn run(app: App, args: Args) -> Result<()> { 15 | let string = if let Some(t) = args.downloadable { 16 | t 17 | } else { 18 | app.prompt_string("What to download")? 19 | }; 20 | 21 | let dl = app.dl_from_string(&string).await?; 22 | 23 | app.download( 24 | &dl, 25 | PathBuf::from("."), 26 | app.multi_progress.add(ProgressBar::new_spinner()), 27 | ) 28 | .await?; 29 | 30 | Ok(()) 31 | } 32 | -------------------------------------------------------------------------------- /src/commands/eject.rs: -------------------------------------------------------------------------------- 1 | use std::fs; 2 | 3 | use anyhow::Result; 4 | use console::style; 5 | use dialoguer::{theme::ColorfulTheme, Input}; 6 | 7 | use crate::app::App; 8 | 9 | #[allow(unused_must_use)] 10 | pub fn run(app: &App) -> Result<()> { 11 | if Input::with_theme(&ColorfulTheme::default()) 12 | .with_prompt("Are you sure you want to delete everything? This is irreversible. Type this server's name to confirm.") 13 | .default(String::new()) 14 | .interact_text()? == app.server.name { 15 | println!(" > {}", style("Deleting server.toml...").yellow()); 16 | let _ = fs::remove_file(app.server.path.join("server.toml")); 17 | 18 | println!(" > {}", style("Deleting config/...").yellow()); 19 | let _ = fs::remove_dir_all(app.server.path.join("config")); 20 | 21 | println!(" > {}", style("Deleting server/...").yellow()); 22 | let _ = fs::remove_dir_all(app.server.path.join("server")); 23 | println!(" > Ejected successfully."); 24 | } else { 25 | println!(" > {}", style("Cancelled").green().bold()); 26 | } 27 | 28 | Ok(()) 29 | } 30 | -------------------------------------------------------------------------------- /src/commands/env/docker.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{Context, Result}; 2 | 3 | use crate::{ 4 | app::App, 5 | util::env::{write_dockerfile, write_dockerignore}, 6 | }; 7 | 8 | pub fn run(app: &App) -> Result<()> { 9 | write_dockerfile(&app.server.path).context("writing Dockerfile")?; 10 | write_dockerignore(&app.server.path).context("writing .dockerignore")?; 11 | 12 | app.success("Default docker files were written successfully"); 13 | 14 | Ok(()) 15 | } 16 | -------------------------------------------------------------------------------- /src/commands/env/gitignore.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | 3 | use crate::{app::App, util::env::write_git}; 4 | 5 | pub fn run(app: &App) -> Result<()> { 6 | write_git()?; 7 | 8 | app.success("Configured gitignore and gitattributes"); 9 | 10 | Ok(()) 11 | } 12 | -------------------------------------------------------------------------------- /src/commands/env/mod.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | 3 | use crate::app::App; 4 | 5 | mod docker; 6 | mod gitignore; 7 | mod workflow_packwiz; 8 | mod workflow_test; 9 | 10 | #[derive(clap::Subcommand, Clone, Copy)] 11 | pub enum Commands { 12 | /// Modify the gitignore 13 | Gitignore, 14 | /// Write the default Dockerfile and .dockerignore 15 | Docker, 16 | /// github workflow: test the server 17 | Test, 18 | /// github workflow: export packwiz automatically 19 | Packwiz, 20 | } 21 | 22 | pub fn run(app: &App, commands: Commands) -> Result<()> { 23 | match commands { 24 | Commands::Gitignore => gitignore::run(app), 25 | Commands::Docker => docker::run(app), 26 | Commands::Packwiz => workflow_packwiz::run(app), 27 | Commands::Test => workflow_test::run(app), 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/commands/env/workflow_packwiz.rs: -------------------------------------------------------------------------------- 1 | use std::{io::Write, path::Path}; 2 | 3 | use anyhow::Result; 4 | use std::fs::File; 5 | 6 | use crate::{app::App, util::env::get_git_root}; 7 | 8 | pub fn run(app: &App) -> Result<()> { 9 | let path = Path::new(&get_git_root().unwrap_or(".".to_owned())) 10 | .join(".github") 11 | .join("workflows"); 12 | 13 | let mut f = File::create(path.join("packwiz.yml"))?; 14 | f.write_all(include_bytes!("../../../res/workflows/packwiz.yml"))?; 15 | app.success("packwiz.yml workflow created"); 16 | 17 | Ok(()) 18 | } 19 | -------------------------------------------------------------------------------- /src/commands/env/workflow_test.rs: -------------------------------------------------------------------------------- 1 | use std::{io::Write, path::Path}; 2 | 3 | use anyhow::Result; 4 | use std::fs::File; 5 | 6 | use crate::{app::App, util::env::get_git_root}; 7 | 8 | pub fn run(app: &App) -> Result<()> { 9 | let path = Path::new(&get_git_root().unwrap_or(".".to_owned())) 10 | .join(".github") 11 | .join("workflows"); 12 | 13 | let mut f = File::create(path.join("test.yml"))?; 14 | f.write_all(include_bytes!("../../../res/workflows/test.yml"))?; 15 | app.success("test.yml workflow created"); 16 | 17 | Ok(()) 18 | } 19 | -------------------------------------------------------------------------------- /src/commands/export/mod.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | 3 | use crate::app::App; 4 | 5 | mod mrpack; 6 | mod packwiz; 7 | 8 | #[derive(clap::Subcommand)] 9 | #[command(subcommand_required = true, arg_required_else_help = true)] 10 | pub enum Commands { 11 | Mrpack(mrpack::Args), 12 | #[command(visible_alias = "pw")] 13 | Packwiz(packwiz::Args), 14 | } 15 | 16 | pub async fn run(app: App, commands: Commands) -> Result<()> { 17 | match commands { 18 | Commands::Mrpack(args) => mrpack::run(app, args).await, 19 | Commands::Packwiz(args) => packwiz::run(app, args).await, 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/commands/export/mrpack.rs: -------------------------------------------------------------------------------- 1 | use std::path::PathBuf; 2 | 3 | use anyhow::{Context, Result}; 4 | 5 | use crate::{app::App, interop::mrpack::MRPackWriter}; 6 | use std::fs::File; 7 | 8 | #[derive(clap::Args)] 9 | pub struct Args { 10 | /// Export as filename 11 | filename: Option, 12 | /// Set the version ID of the mrpack 13 | #[arg(long, short)] 14 | version: Option, 15 | } 16 | 17 | pub async fn run(mut app: App, args: Args) -> Result<()> { 18 | let s = app 19 | .server 20 | .name 21 | .clone() 22 | .replace(|c: char| !c.is_alphanumeric(), ""); 23 | 24 | let default_output = 25 | PathBuf::from(if s.is_empty() { "server".to_owned() } else { s } + ".mrpack"); 26 | 27 | let output_filename = args.filename.unwrap_or(default_output); 28 | 29 | let output_filename = if output_filename.extension().is_none() { 30 | output_filename.with_extension("mrpack") 31 | } else { 32 | output_filename 33 | }; 34 | 35 | let output_file = File::create(output_filename).context("Creating mrpack output file")?; 36 | 37 | app.mrpack() 38 | .export_all(MRPackWriter::from_writer(output_file)) 39 | .await?; 40 | 41 | Ok(()) 42 | } 43 | -------------------------------------------------------------------------------- /src/commands/export/packwiz.rs: -------------------------------------------------------------------------------- 1 | use std::{fs, path::PathBuf}; 2 | 3 | use anyhow::Result; 4 | 5 | use crate::app::App; 6 | 7 | #[derive(clap::Args)] 8 | pub struct Args { 9 | #[arg(long, short)] 10 | /// The output directory for the packwiz files 11 | output: Option, 12 | } 13 | 14 | pub async fn run(mut app: App, args: Args) -> Result<()> { 15 | let default_output = app.server.path.join("pack"); 16 | let output_dir = args.output.unwrap_or(default_output); 17 | 18 | fs::create_dir_all(&output_dir)?; 19 | 20 | app.packwiz().export_all(output_dir).await?; 21 | 22 | Ok(()) 23 | } 24 | -------------------------------------------------------------------------------- /src/commands/import/datapack.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | 3 | use crate::app::App; 4 | 5 | #[derive(clap::Args, Default)] 6 | pub struct Args { 7 | url: Option, 8 | } 9 | 10 | pub async fn run(mut app: App, args: Args) -> Result<()> { 11 | let urlstr = match args.url { 12 | Some(url) => url.clone(), 13 | None => app.prompt_string("URL")?, 14 | }; 15 | 16 | app.add_datapack(app.dl_from_string(&urlstr).await?)?; 17 | 18 | app.save_changes()?; 19 | app.refresh_markdown().await?; 20 | 21 | Ok(()) 22 | } 23 | -------------------------------------------------------------------------------- /src/commands/import/mod.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | 3 | use crate::app::App; 4 | 5 | mod datapack; 6 | mod mrpack; 7 | mod packwiz; 8 | mod url; 9 | 10 | #[derive(clap::Subcommand)] 11 | pub enum Commands { 12 | /// Import from a URL 13 | Url(url::Args), 14 | /// Import datapack from url 15 | #[command(visible_alias = "dp")] 16 | Datapack(datapack::Args), 17 | /// Import from .mrpack (modrinth modpacks) 18 | Mrpack(mrpack::Args), 19 | /// Import from packwiz 20 | #[command(visible_alias = "pw")] 21 | Packwiz(packwiz::Args), 22 | } 23 | 24 | impl Default for Commands { 25 | fn default() -> Self { 26 | Self::Url(url::Args::default()) 27 | } 28 | } 29 | 30 | pub async fn run(app: App, subcommand: Commands) -> Result<()> { 31 | match subcommand { 32 | Commands::Url(args) => url::run(app, args).await?, 33 | Commands::Datapack(args) => datapack::run(app, args).await?, 34 | Commands::Mrpack(args) => mrpack::run(app, args).await?, 35 | Commands::Packwiz(args) => packwiz::run(app, args).await?, 36 | } 37 | Ok(()) 38 | } 39 | -------------------------------------------------------------------------------- /src/commands/import/mrpack.rs: -------------------------------------------------------------------------------- 1 | use std::path::Path; 2 | 3 | use anyhow::Result; 4 | use indicatif::ProgressBar; 5 | use tempfile::Builder; 6 | 7 | use crate::{app::App, interop::mrpack::MRPackReader, model::Downloadable}; 8 | use std::fs::File; 9 | 10 | #[derive(clap::Args, Default)] 11 | pub struct Args { 12 | source: String, 13 | #[arg(long)] 14 | keep: bool, 15 | } 16 | 17 | pub async fn run(mut app: App, args: Args) -> Result<()> { 18 | let src = args.source; 19 | 20 | let tmp_dir = Builder::new().prefix("mcman-mrpack-import").tempdir()?; 21 | 22 | let f = if Path::new(&src).exists() { 23 | File::open(&src)? 24 | } else { 25 | let dl = if src.starts_with("http") && src.ends_with(".mrpack") { 26 | Downloadable::Url { 27 | url: src, 28 | filename: None, 29 | desc: None, 30 | } 31 | } else { 32 | app.dl_from_string(&src).await? 33 | }; 34 | let resolved = app 35 | .download( 36 | &dl, 37 | tmp_dir.path().to_path_buf(), 38 | ProgressBar::new_spinner(), 39 | ) 40 | .await?; 41 | let path = tmp_dir.path().join(resolved.filename); 42 | File::open(path)? 43 | }; 44 | 45 | if !args.keep { 46 | app.server.mods.clear(); 47 | app.info("cleared mods list"); 48 | } 49 | 50 | app.mrpack() 51 | .import_all(MRPackReader::from_reader(f)?, None) 52 | .await?; 53 | 54 | app.save_changes()?; 55 | app.refresh_markdown().await?; 56 | 57 | Ok(()) 58 | } 59 | -------------------------------------------------------------------------------- /src/commands/import/packwiz.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | 3 | use crate::app::App; 4 | 5 | #[derive(clap::Args, Default)] 6 | pub struct Args { 7 | source: String, 8 | } 9 | 10 | pub async fn run(mut app: App, args: Args) -> Result<()> { 11 | app.packwiz().import_all(&args.source).await?; 12 | 13 | app.save_changes()?; 14 | app.refresh_markdown().await?; 15 | 16 | Ok(()) 17 | } 18 | -------------------------------------------------------------------------------- /src/commands/import/url.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | 3 | use crate::app::{App, Prefix}; 4 | 5 | #[derive(clap::Args, Default)] 6 | pub struct Args { 7 | pub url: Option, 8 | } 9 | 10 | pub async fn run(mut app: App, args: Args) -> Result<()> { 11 | let urlstr = match args.url { 12 | Some(url) => url.clone(), 13 | None => app.prompt_string("URL or shortcode?")?, 14 | }; 15 | 16 | let addon = app.dl_from_string(&urlstr).await?; 17 | let addon_name = addon.to_short_string(); 18 | 19 | app.add_addon_inferred(addon)?; 20 | 21 | app.save_changes()?; 22 | app.notify(Prefix::Imported, addon_name); 23 | app.refresh_markdown().await?; 24 | 25 | Ok(()) 26 | } 27 | -------------------------------------------------------------------------------- /src/commands/info.rs: -------------------------------------------------------------------------------- 1 | use crate::util::md::MarkdownTable; 2 | use crate::{app::App, model::Server}; 3 | use anyhow::{Context, Result}; 4 | use console::style; 5 | use indexmap::IndexMap; 6 | 7 | pub fn run(app: &App) -> Result<()> { 8 | let server = Server::load().context("Failed to load server.toml")?; 9 | 10 | let table = app.markdown().table_server(); 11 | 12 | let mut server_info = IndexMap::new(); 13 | 14 | for idx in 0..table.rows[0].len() { 15 | let k = table.headers[idx].clone(); 16 | let v = table.rows[0][idx].clone(); 17 | 18 | server_info.insert(k, v); 19 | } 20 | 21 | let pad_keys = server_info 22 | .iter() 23 | .max_by_key(|(k, _)| k.len()) 24 | .unwrap() 25 | .0 26 | .len(); 27 | 28 | for (k, v) in &server_info { 29 | let k_styled = style(format!("{k:pad_keys$}")).cyan().bold(); 30 | 31 | println!(" {k_styled}: {v}"); 32 | } 33 | 34 | if !server.plugins.is_empty() { 35 | println!( 36 | " {:pad_keys$}> {} {}", 37 | "", 38 | style(server.plugins.len()).bold(), 39 | style("Plugins").cyan(), 40 | ); 41 | 42 | let mut table = MarkdownTable::new(); 43 | 44 | for plugin in &server.plugins { 45 | table.add_from_map(plugin.fields_to_map()); 46 | } 47 | 48 | let text = table.render_ascii(); 49 | 50 | println!("{text}"); 51 | } 52 | 53 | if !server.mods.is_empty() { 54 | println!( 55 | " {:pad_keys$}> {} {}", 56 | "", 57 | style(server.mods.len()).bold(), 58 | style("Mods").cyan(), 59 | ); 60 | 61 | let mut table = MarkdownTable::new(); 62 | 63 | for addon in &server.mods { 64 | table.add_from_map(addon.fields_to_map()); 65 | } 66 | 67 | let text = table.render_ascii(); 68 | 69 | println!("{text}"); 70 | } 71 | 72 | Ok(()) 73 | } 74 | -------------------------------------------------------------------------------- /src/commands/markdown.rs: -------------------------------------------------------------------------------- 1 | use console::style; 2 | use dialoguer::theme::ColorfulTheme; 3 | use dialoguer::Confirm; 4 | 5 | use crate::app::App; 6 | use anyhow::Result; 7 | 8 | pub async fn run(mut app: App) -> Result<()> { 9 | if app.server.markdown.files.is_empty() { 10 | println!(" ! {}", style("No markdown files were found").yellow()); 11 | 12 | if Confirm::with_theme(&ColorfulTheme::default()) 13 | .with_prompt("Add and use README.md?") 14 | .interact()? 15 | { 16 | app.server.markdown.files.push("README.md".to_owned()); 17 | app.server.save()?; 18 | } else { 19 | return Ok(()); 20 | } 21 | } 22 | 23 | app.markdown().update_files().await?; 24 | 25 | Ok(()) 26 | } 27 | -------------------------------------------------------------------------------- /src/commands/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod add; 2 | pub mod build; 3 | pub mod cache; 4 | pub mod dev; 5 | pub mod download; 6 | pub mod eject; 7 | pub mod env; 8 | pub mod export; 9 | pub mod import; 10 | pub mod info; 11 | pub mod init; 12 | pub mod markdown; 13 | pub mod pull; 14 | pub mod run; 15 | pub mod version; 16 | pub mod world; 17 | 18 | #[cfg(feature = "autocomplete")] 19 | pub mod completions; 20 | -------------------------------------------------------------------------------- /src/commands/pull.rs: -------------------------------------------------------------------------------- 1 | use std::{fs, path::PathBuf, time::Duration}; 2 | 3 | use anyhow::{anyhow, bail, Context, Result}; 4 | use console::style; 5 | use dialoguer::theme::ColorfulTheme; 6 | use glob::glob; 7 | use indicatif::ProgressBar; 8 | use pathdiff::diff_paths; 9 | 10 | use crate::app::App; 11 | 12 | #[derive(clap::Args)] 13 | pub struct Args { 14 | /// Files to pull 15 | #[arg(required = true)] 16 | file: String, 17 | } 18 | 19 | pub fn run(app: &App, args: Args) -> Result<()> { 20 | let files = args.file; 21 | 22 | let pb = app 23 | .multi_progress 24 | .add(ProgressBar::new_spinner()) 25 | .with_message("Pulling files..."); 26 | 27 | pb.enable_steady_tick(Duration::from_millis(250)); 28 | 29 | let mut count = 0; 30 | let mut skipped = 0; 31 | 32 | for entry in glob(&files)? { 33 | let entry = entry?; 34 | 35 | let diff = diff_paths(&entry, fs::canonicalize(&app.server.path)?) 36 | .ok_or(anyhow!("Cannot diff paths"))?; 37 | 38 | if !diff.starts_with("server") { 39 | bail!("You aren't inside server/"); 40 | } 41 | 42 | let mut destination = PathBuf::new(); 43 | let mut iter = diff.components(); 44 | iter.next().expect("Path to have atleast 1 component"); 45 | destination.push(&app.server.path); 46 | destination.push("config"); 47 | destination.extend(iter); 48 | 49 | fs::create_dir_all(destination.parent().unwrap()).context("Failed to create dirs")?; 50 | 51 | if destination.exists() { 52 | if app.confirm(&format!( 53 | "File '{}' already exists, overwrite?", 54 | destination.display() 55 | ))? { 56 | app.info(format!("Skipped {}", destination.display())); 57 | skipped += 1; 58 | } else { 59 | continue; 60 | } 61 | } 62 | 63 | fs::copy(&entry, &destination)?; 64 | 65 | app.multi_progress.println(format!( 66 | " {} {} {} {}", 67 | ColorfulTheme::default().picked_item_prefix, 68 | style(&diff.to_string_lossy()).dim(), 69 | style("=>").bold(), 70 | style( 71 | diff_paths( 72 | fs::canonicalize(&destination)?, 73 | fs::canonicalize(&app.server.path)? 74 | ) 75 | .unwrap_or_default() 76 | .to_string_lossy() 77 | ) 78 | .dim() 79 | ))?; 80 | 81 | count += 1; 82 | } 83 | 84 | pb.finish_with_message(format!( 85 | " {} Pulled {} files to {}", 86 | ColorfulTheme::default().picked_item_prefix, 87 | count, 88 | style("config/").bold(), 89 | )); 90 | 91 | if skipped != 0 { 92 | app.warn(format!("Skipped {skipped} files")); 93 | } 94 | 95 | Ok(()) 96 | } 97 | -------------------------------------------------------------------------------- /src/commands/run.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | 3 | use crate::{app::App, hot_reload::DevSession}; 4 | 5 | use super::build::BuildArgs; 6 | 7 | #[derive(clap::Args)] 8 | pub struct RunArgs { 9 | #[command(flatten)] 10 | build_args: BuildArgs, 11 | /// Test the server (stops it when it ends startup) 12 | #[arg(long)] 13 | test: bool, 14 | } 15 | 16 | impl RunArgs { 17 | pub fn create_dev_session(self, app: &mut App) -> Result> { 18 | let builder = self.build_args.create_build_context(app)?; 19 | 20 | Ok(DevSession { 21 | builder, 22 | jar_name: None, 23 | hot_reload: None, 24 | test_mode: self.test, 25 | }) 26 | } 27 | } 28 | 29 | pub async fn run(mut app: App, args: RunArgs) -> Result<()> { 30 | let dev_session = args.create_dev_session(&mut app)?; 31 | dev_session.start().await?; 32 | 33 | println!(); 34 | 35 | Ok(()) 36 | } 37 | -------------------------------------------------------------------------------- /src/commands/version.rs: -------------------------------------------------------------------------------- 1 | use std::cmp::Ordering; 2 | 3 | use anyhow::Result; 4 | use console::style; 5 | use semver::Version; 6 | 7 | use crate::app::BaseApp; 8 | 9 | #[derive(clap::Args)] 10 | pub struct Args { 11 | /// Only print the version 12 | #[arg(long)] 13 | pub plain: bool, 14 | } 15 | 16 | pub async fn run(base_app: BaseApp, args: Args) -> Result<()> { 17 | if args.plain { 18 | println!("{}", env!("CARGO_PKG_VERSION")); 19 | } else { 20 | println!( 21 | " > {} by {}\n version {}\n\n {}", 22 | style(env!("CARGO_PKG_NAME")).green().bold(), 23 | style(env!("CARGO_PKG_AUTHORS")).magenta().bold(), 24 | style(env!("CARGO_PKG_VERSION")).bold(), 25 | style("> checking for updates...").dim() 26 | ); 27 | 28 | let repo_name: String = env!("CARGO_PKG_REPOSITORY").chars().skip(19).collect(); 29 | 30 | let app = base_app.upgrade_with_default_server()?; 31 | 32 | let releases = app.github().fetch_releases(&repo_name).await?; 33 | 34 | let latest_ver = Version::parse(&releases.first().unwrap().tag_name)?; 35 | 36 | match Version::parse(env!("CARGO_PKG_VERSION"))?.cmp(&latest_ver) { 37 | Ordering::Equal => { 38 | println!(" > up to date!"); 39 | } 40 | Ordering::Greater => { 41 | println!(" {}", style("> version is newer (dev/unreleased)").yellow()); 42 | } 43 | Ordering::Less => { 44 | println!( 45 | " {}\n {} {} => {}\n {} {}/releases/tag/{latest_ver}", 46 | style("> A new version is available!").cyan(), 47 | style("|").cyan(), 48 | style(env!("CARGO_PKG_VERSION")).red(), 49 | style(&latest_ver).green().bold(), 50 | style("|").cyan(), 51 | env!("CARGO_PKG_REPOSITORY"), 52 | ); 53 | } 54 | } 55 | } 56 | 57 | Ok(()) 58 | } 59 | -------------------------------------------------------------------------------- /src/commands/world/mod.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | 3 | use crate::app::App; 4 | 5 | mod pack; 6 | mod unpack; 7 | 8 | #[derive(clap::Subcommand)] 9 | pub enum Commands { 10 | #[command(visible_alias = "zip")] 11 | Pack(pack::Args), 12 | #[command(visible_alias = "unzip")] 13 | Unpack(unpack::Args), 14 | } 15 | 16 | pub fn run(app: &mut App, commands: Commands) -> Result<()> { 17 | match commands { 18 | Commands::Pack(args) => pack::run(app, args), 19 | Commands::Unpack(args) => unpack::run(app, args), 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/commands/world/pack.rs: -------------------------------------------------------------------------------- 1 | use crate::app::App; 2 | use anyhow::Result; 3 | 4 | #[derive(clap::Args)] 5 | pub struct Args { 6 | /// The world to pack - packs every world if not present 7 | world: Option, 8 | } 9 | 10 | pub fn run(app: &mut App, args: Args) -> Result<()> { 11 | let world_name = if let Some(s) = args.world { 12 | s 13 | } else { 14 | app.select_world("Pack...")? 15 | }; 16 | 17 | app.worlds().pack(&world_name)?; 18 | 19 | Ok(()) 20 | } 21 | -------------------------------------------------------------------------------- /src/commands/world/unpack.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | 3 | use crate::app::App; 4 | 5 | #[derive(clap::Args)] 6 | pub struct Args { 7 | /// The world to unpack 8 | world: Option, 9 | } 10 | 11 | pub fn run(app: &mut App, args: Args) -> Result<()> { 12 | let world_name = if let Some(s) = args.world { 13 | s 14 | } else { 15 | app.select_world("Unpack...")? 16 | }; 17 | 18 | app.worlds().unpack(&world_name)?; 19 | 20 | Ok(()) 21 | } 22 | -------------------------------------------------------------------------------- /src/commands/ws/mod.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | 3 | use crate::app::App; 4 | 5 | pub async fn run(_app: App) -> Result<()> { 6 | Ok(()) 7 | } 8 | -------------------------------------------------------------------------------- /src/core/addons.rs: -------------------------------------------------------------------------------- 1 | use std::{collections::HashSet, time::Duration}; 2 | 3 | use anyhow::Result; 4 | use indicatif::{FormattedDuration, ProgressBar, ProgressIterator, ProgressStyle}; 5 | use tokio::fs; 6 | 7 | use crate::app::AddonType; 8 | 9 | use super::BuildContext; 10 | 11 | impl<'a> BuildContext<'a> { 12 | pub async fn download_addons(&mut self, addon_type: AddonType) -> Result<()> { 13 | let server_list = self.app.get_addons(addon_type); 14 | let addons = match addon_type { 15 | AddonType::Plugin => &self.lockfile.plugins, 16 | AddonType::Mod => &self.lockfile.mods, 17 | }; 18 | 19 | let existing_files = addons.iter().fold( 20 | HashSet::with_capacity(addons.len()), 21 | |mut hash_set, (_, res)| { 22 | hash_set.insert(res.filename.clone()); 23 | hash_set 24 | }, 25 | ); 26 | 27 | if server_list.is_empty() && existing_files.is_empty() { 28 | return Ok(()); 29 | } 30 | 31 | self.app.print_job(&format!( 32 | "Processing {} {addon_type}{}...{}", 33 | server_list.len(), 34 | if server_list.len() == 1 { "" } else { "s" }, 35 | if server_list.len() < 200 { 36 | "" 37 | } else { 38 | " may god help you" 39 | }, 40 | )); 41 | 42 | self.app.ci(&format!("::group::Processing {addon_type}s")); 43 | 44 | let mut files_list = HashSet::with_capacity(server_list.len()); 45 | 46 | let pb = ProgressBar::new(server_list.len() as u64) 47 | .with_style(ProgressStyle::with_template( 48 | "{msg} [{wide_bar:.cyan/blue}] {pos}/{len}", 49 | )?) 50 | .with_message(format!("Processing {addon_type}s")); 51 | 52 | let pb = self.app.multi_progress.add(pb); 53 | 54 | for addon in server_list.iter().progress_with(pb.clone()) { 55 | let (_path, resolved) = self 56 | .downloadable(addon, addon_type.folder(), Some(&pb)) 57 | .await?; 58 | 59 | files_list.insert(resolved.filename.clone()); 60 | 61 | match addon_type { 62 | AddonType::Plugin => &mut self.new_lockfile.plugins, 63 | AddonType::Mod => &mut self.new_lockfile.mods, 64 | } 65 | .push((addon.clone(), resolved)); 66 | } 67 | 68 | pb.set_style(ProgressStyle::with_template( 69 | "{spinner:.blue} {prefix:.yellow} {msg}", 70 | )?); 71 | pb.set_prefix("Deleting"); 72 | pb.enable_steady_tick(Duration::from_micros(250)); 73 | 74 | for removed_file in existing_files.difference(&files_list) { 75 | pb.set_message(removed_file.clone()); 76 | fs::remove_file(self.output_dir.join(addon_type.folder()).join(removed_file)).await?; 77 | } 78 | 79 | pb.finish_and_clear(); 80 | 81 | if files_list.len() >= 10 { 82 | self.app.success(format!( 83 | "Processed {} {addon_type}{} in {}", 84 | files_list.len(), 85 | if files_list.len() == 1 { "" } else { "s" }, 86 | FormattedDuration(pb.elapsed()) 87 | )); 88 | } 89 | 90 | self.app.ci("::endgroup::"); 91 | 92 | Ok(()) 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src/core/scripts.rs: -------------------------------------------------------------------------------- 1 | use std::{fs::OpenOptions, io::Write}; 2 | 3 | use anyhow::Result; 4 | use tokio::fs; 5 | 6 | use crate::model::{ServerType, StartupMethod}; 7 | 8 | use super::BuildContext; 9 | 10 | impl<'a> BuildContext<'a> { 11 | pub async fn get_startup_method(&self, serverjar_name: &str) -> Result { 12 | let mcver = self.app.mc_version(); 13 | Ok(match &self.app.server.jar { 14 | ServerType::NeoForge { loader } => { 15 | let l = self.app.neoforge().resolve_version(loader).await?; 16 | 17 | StartupMethod::Custom { 18 | windows: vec![format!( 19 | "@libraries/net/neoforged/forge/{mcver}-{l}/win_args.txt" 20 | )], 21 | linux: vec![format!( 22 | "@libraries/net/neoforged/forge/{mcver}-{l}/unix_args.txt" 23 | )], 24 | } 25 | } 26 | ServerType::Forge { loader } => { 27 | let l = self.app.forge().resolve_version(loader).await?; 28 | 29 | StartupMethod::Custom { 30 | windows: vec![format!( 31 | "@libraries/net/minecraftforge/forge/{mcver}-{l}/win_args.txt" 32 | )], 33 | linux: vec![format!( 34 | "@libraries/net/minecraftforge/forge/{mcver}-{l}/unix_args.txt" 35 | )], 36 | } 37 | } 38 | _ => StartupMethod::Jar(serverjar_name.to_owned()), 39 | }) 40 | } 41 | 42 | pub async fn create_scripts(&self, startup: StartupMethod) -> Result<()> { 43 | fs::write( 44 | self.output_dir.join("start.bat"), 45 | self.app 46 | .server 47 | .launcher 48 | .generate_script_win(&self.app.server.name, &startup), 49 | ) 50 | .await?; 51 | 52 | let mut file; 53 | #[cfg(target_family = "unix")] 54 | { 55 | use std::os::unix::prelude::OpenOptionsExt; 56 | file = OpenOptions::new() 57 | .create(true) 58 | .write(true) 59 | .mode(0o755) 60 | .open(self.output_dir.join("start.sh"))?; 61 | } 62 | #[cfg(not(target_family = "unix"))] 63 | { 64 | file = OpenOptions::new() 65 | .create(true) 66 | .write(true) 67 | .open(self.output_dir.join("start.sh"))?; 68 | } 69 | 70 | file.write_all( 71 | self.app 72 | .server 73 | .launcher 74 | .generate_script_linux(&self.app.server.name, &startup) 75 | .as_bytes(), 76 | )?; 77 | 78 | Ok(()) 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/hot_reload/config.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | collections::HashMap, 3 | fs::{self, File}, 4 | io::Write, 5 | path::PathBuf, 6 | }; 7 | 8 | use anyhow::{anyhow, Result}; 9 | use glob::Pattern; 10 | use serde::{Deserialize, Serialize}; 11 | 12 | #[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Default)] 13 | #[serde(tag = "type", try_from = "String", into = "String")] 14 | pub enum HotReloadAction { 15 | #[default] 16 | Reload, 17 | Restart, 18 | #[serde(alias = "run")] 19 | RunCommand(String), 20 | } 21 | 22 | impl TryFrom for HotReloadAction { 23 | type Error = anyhow::Error; 24 | 25 | fn try_from(value: String) -> std::result::Result { 26 | if value.starts_with('/') { 27 | Ok(Self::RunCommand( 28 | value.strip_prefix('/').unwrap().to_string(), 29 | )) 30 | } else { 31 | match value.to_lowercase().as_str() { 32 | "reload" => Ok(Self::Reload), 33 | "restart" => Ok(Self::Restart), 34 | _ => Err(anyhow!("Cant parse HotReloadAction: {value}")), 35 | } 36 | } 37 | } 38 | } 39 | 40 | impl From for String { 41 | fn from(val: HotReloadAction) -> Self { 42 | match val { 43 | HotReloadAction::Reload => String::from("reload"), 44 | HotReloadAction::Restart => String::from("restart"), 45 | HotReloadAction::RunCommand(cmd) => format!("/{cmd}"), 46 | } 47 | } 48 | } 49 | 50 | #[derive(Debug, Serialize, Deserialize)] 51 | pub struct HotReloadConfig { 52 | #[serde(skip)] 53 | pub path: PathBuf, 54 | 55 | #[serde(skip_serializing_if = "Vec::is_empty", default = "Vec::default")] 56 | pub files: Vec, 57 | #[serde(skip_serializing_if = "HashMap::is_empty", default)] 58 | pub events: HashMap, 59 | } 60 | 61 | impl Default for HotReloadConfig { 62 | fn default() -> Self { 63 | Self { 64 | path: PathBuf::from("./hotreload.toml"), 65 | events: HashMap::new(), 66 | files: vec![Entry { 67 | path: Pattern::new("server.properties").unwrap(), 68 | action: HotReloadAction::Reload, 69 | }], 70 | } 71 | } 72 | } 73 | 74 | impl HotReloadConfig { 75 | pub fn load_from(path: &PathBuf) -> Result { 76 | let data = fs::read_to_string(path)?; 77 | let mut h: Self = toml::from_str(&data)?; 78 | h.path = path.to_owned(); 79 | Ok(h) 80 | } 81 | 82 | pub fn save(&self) -> Result<()> { 83 | let cfg_str = toml::to_string_pretty(&self)?; 84 | let mut f = File::create(&self.path)?; 85 | f.write_all(cfg_str.as_bytes())?; 86 | 87 | Ok(()) 88 | } 89 | } 90 | 91 | #[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] 92 | pub struct Entry { 93 | #[serde(with = "super::pattern_serde")] 94 | pub path: Pattern, 95 | pub action: HotReloadAction, 96 | } 97 | -------------------------------------------------------------------------------- /src/hot_reload/pattern_serde.rs: -------------------------------------------------------------------------------- 1 | use glob::Pattern; 2 | use serde::{Deserialize, Deserializer, Serialize, Serializer}; 3 | 4 | pub fn serialize(pat: &Pattern, ser: S) -> Result { 5 | pat.as_str().serialize(ser) 6 | } 7 | 8 | pub fn deserialize<'de, D: Deserializer<'de>>(de: D) -> Result { 9 | Pattern::new(&String::deserialize(de)?).map_err(serde::de::Error::custom) 10 | } 11 | -------------------------------------------------------------------------------- /src/interop/java.rs: -------------------------------------------------------------------------------- 1 | /* 2 | use std::path::PathBuf; 3 | 4 | use crate::app::App; 5 | 6 | pub struct JavaVersion(pub PathBuf, pub String); 7 | 8 | pub struct JavaAPI<'a>(pub &'a App); 9 | 10 | impl<'a> JavaAPI<'a> { 11 | 12 | } 13 | */ 14 | -------------------------------------------------------------------------------- /src/interop/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod hooks; 2 | pub mod java; 3 | pub mod markdown; 4 | pub mod mrpack; 5 | pub mod packwiz; 6 | pub mod worlds; 7 | -------------------------------------------------------------------------------- /src/model/app_config/mod.rs: -------------------------------------------------------------------------------- 1 | use confique::Config; 2 | use serde::{Deserialize, Serialize}; 3 | 4 | #[derive(Debug, Serialize, Deserialize, Config)] 5 | pub struct MCLogsService { 6 | #[config(env = "upload_to_mclogs", default = false)] 7 | pub enabled: bool, 8 | } 9 | 10 | #[derive(Debug, Serialize, Deserialize, Config)] 11 | pub struct Services { 12 | #[config(nested)] 13 | pub mclogs: MCLogsService, 14 | } 15 | 16 | #[derive(Debug, Serialize, Deserialize, Config)] 17 | pub struct Sources { 18 | #[config(nested)] 19 | pub github: GithubSource, 20 | } 21 | 22 | #[derive(Debug, Serialize, Deserialize, Config)] 23 | pub struct GithubSource { 24 | #[config(env = "GITHUB_TOKEN")] 25 | pub api_token: Option, 26 | #[config(env = "GITHUB_API_URL", default = "https://api.github.com")] 27 | pub api_url: String, 28 | } 29 | 30 | #[derive(Debug, Serialize, Deserialize, Config)] 31 | pub struct AppConfig { 32 | #[config(default = [])] 33 | pub disable_cache: Vec, 34 | #[config(nested)] 35 | pub services: Services, 36 | #[config(nested)] 37 | pub sources: Sources, 38 | #[config(env = "JAVA_BIN", default = "java")] 39 | pub default_java: String, 40 | } 41 | -------------------------------------------------------------------------------- /src/model/clientsidemod.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | 3 | use super::Downloadable; 4 | 5 | #[derive(Debug, Serialize, Deserialize, Clone, Hash, PartialEq, Eq)] 6 | pub struct ClientSideMod { 7 | #[serde(flatten)] 8 | pub dl: Downloadable, 9 | 10 | pub optional: bool, 11 | pub desc: String, 12 | } 13 | -------------------------------------------------------------------------------- /src/model/downloadable/meta.rs: -------------------------------------------------------------------------------- 1 | use super::Downloadable; 2 | 3 | impl Downloadable { 4 | /// Check if just the version is differient from other 5 | pub fn is_same_as(&self, other: &Self) -> bool { 6 | match (self, other) { 7 | (Self::Hangar { id: a, .. }, Self::Hangar { id: b, .. }) if a == b => true, 8 | (Self::CurseRinth { id: a, .. }, Self::CurseRinth { id: b, .. }) if a == b => true, 9 | (Self::Modrinth { id: a, .. }, Self::Modrinth { id: b, .. }) if a == b => true, 10 | (Self::Spigot { id: a, .. }, Self::Spigot { id: b, .. }) if a == b => true, 11 | (Self::Url { url: a, .. }, Self::Url { url: b, .. }) if a == b => true, 12 | _ => self == other, 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/model/hooks.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | 3 | use serde::{Deserialize, Serialize}; 4 | 5 | #[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Default)] 6 | pub struct Hook { 7 | pub when: HookEvent, 8 | #[serde(default)] 9 | pub onfail: HookFailBehavior, 10 | #[serde(default = "bool_true")] 11 | pub show_output: bool, 12 | #[serde(default)] 13 | pub description: String, 14 | #[serde(default)] 15 | pub disabled: bool, 16 | #[serde(default)] 17 | pub env: HashMap, 18 | 19 | pub windows: Option, 20 | pub linux: Option, 21 | } 22 | 23 | fn bool_true() -> bool { 24 | true 25 | } 26 | 27 | #[derive(Debug, Serialize, Deserialize, Clone, Hash, PartialEq, Default)] 28 | #[serde(rename_all = "lowercase")] 29 | pub enum HookEvent { 30 | #[default] 31 | None, 32 | PreBuild, 33 | PostBuild, 34 | PreInstall, 35 | PostInstall, 36 | PreWorldUnpack, 37 | PostWorldUnpack, 38 | PreWorldPack, 39 | PostWorldPack, 40 | DevSessionStart, 41 | DevSessionEnd, 42 | TestSuccess, 43 | TestFail, 44 | } 45 | 46 | #[derive(Debug, Serialize, Deserialize, Clone, Hash, PartialEq, Default)] 47 | pub enum HookFailBehavior { 48 | #[default] 49 | Error, 50 | Ignore, 51 | Warn, 52 | } 53 | -------------------------------------------------------------------------------- /src/model/lockfile.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | collections::HashMap, 3 | fs::{read_to_string, File}, 4 | io::BufWriter, 5 | path::{Path, PathBuf}, 6 | time::SystemTime, 7 | }; 8 | 9 | use anyhow::Result; 10 | use serde::{Deserialize, Serialize}; 11 | 12 | use crate::app::ResolvedFile; 13 | 14 | use super::Downloadable; 15 | 16 | #[derive(Debug, Deserialize, Serialize)] 17 | #[serde(default)] 18 | pub struct Lockfile { 19 | #[serde(skip)] 20 | pub path: PathBuf, 21 | 22 | pub plugins: Vec<(Downloadable, ResolvedFile)>, 23 | pub mods: Vec<(Downloadable, ResolvedFile)>, 24 | 25 | pub server_vars: HashMap, 26 | pub nw_vars: HashMap, 27 | 28 | pub files: Vec, 29 | } 30 | 31 | #[derive(Debug, Deserialize, Serialize)] 32 | pub struct BootstrappedFile { 33 | pub path: PathBuf, 34 | pub date: SystemTime, 35 | } 36 | 37 | impl Lockfile { 38 | pub fn get_lockfile(output_dir: &Path) -> Result { 39 | if output_dir.join(".mcman.lock").exists() { 40 | Ok(Self::load_from(&output_dir.join(".mcman.lock"))?) 41 | } else { 42 | Ok(Self { 43 | path: output_dir.join(".mcman.lock"), 44 | ..Default::default() 45 | }) 46 | } 47 | } 48 | 49 | pub fn load_from(path: &PathBuf) -> Result { 50 | let data = read_to_string(path)?; 51 | let mut nw: Self = serde_json::from_str(&data)?; 52 | nw.path = path.to_owned(); 53 | Ok(nw) 54 | } 55 | 56 | pub fn save(&self) -> Result<()> { 57 | let writer = BufWriter::new(File::create(&self.path)?); 58 | 59 | Ok(serde_json::to_writer_pretty(writer, &self)?) 60 | } 61 | } 62 | 63 | impl Default for Lockfile { 64 | fn default() -> Self { 65 | Self { 66 | path: PathBuf::from("./.mcman.lock"), 67 | plugins: vec![], 68 | mods: vec![], 69 | files: vec![], 70 | server_vars: HashMap::default(), 71 | nw_vars: HashMap::default(), 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/model/mod.rs: -------------------------------------------------------------------------------- 1 | mod app_config; 2 | mod clientsidemod; 3 | mod downloadable; 4 | mod hooks; 5 | mod lockfile; 6 | mod network; 7 | mod serverlauncher; 8 | mod servertoml; 9 | mod servertype; 10 | mod world; 11 | 12 | pub use app_config::*; 13 | pub use clientsidemod::*; 14 | pub use downloadable::*; 15 | pub use hooks::*; 16 | pub use lockfile::*; 17 | pub use network::*; 18 | pub use serverlauncher::*; 19 | pub use servertoml::*; 20 | pub use servertype::*; 21 | pub use world::*; 22 | -------------------------------------------------------------------------------- /src/model/network.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | collections::HashMap, 3 | env, 4 | fs::{read_to_string, File}, 5 | io::Write, 6 | path::{Path, PathBuf}, 7 | }; 8 | 9 | use anyhow::Result; 10 | use serde::{Deserialize, Serialize}; 11 | 12 | use super::{Downloadable, Hook, MarkdownOptions}; 13 | 14 | #[derive(Debug, Deserialize, Serialize, Clone)] 15 | #[serde(default)] 16 | pub struct Network { 17 | #[serde(skip)] 18 | pub path: PathBuf, 19 | 20 | pub name: String, 21 | pub proxy: String, 22 | #[serde(default)] 23 | #[serde(skip_serializing_if = "Vec::is_empty")] 24 | pub proxy_groups: Vec, 25 | pub port: u16, 26 | pub servers: HashMap, 27 | pub variables: HashMap, 28 | 29 | #[serde(default)] 30 | #[serde(skip_serializing_if = "MarkdownOptions::is_empty")] 31 | pub markdown: MarkdownOptions, 32 | 33 | #[serde(default)] 34 | #[serde(skip_serializing_if = "HashMap::is_empty")] 35 | pub hooks: HashMap, 36 | 37 | #[serde(default)] 38 | #[serde(skip_serializing_if = "HashMap::is_empty")] 39 | pub groups: HashMap, 40 | } 41 | 42 | #[derive(Debug, Deserialize, Serialize, Default, Clone)] 43 | #[serde(default)] 44 | pub struct ServerEntry { 45 | pub port: u16, 46 | pub ip_address: Option, 47 | #[serde(skip_serializing_if = "Vec::is_empty")] 48 | pub groups: Vec, 49 | } 50 | 51 | #[derive(Debug, Default, Deserialize, Serialize, Clone)] 52 | #[serde(default)] 53 | pub struct Group { 54 | #[serde(default)] 55 | pub variables: HashMap, 56 | #[serde(skip_serializing_if = "Vec::is_empty")] 57 | pub plugins: Vec, 58 | #[serde(skip_serializing_if = "Vec::is_empty")] 59 | pub mods: Vec, 60 | } 61 | 62 | impl Network { 63 | pub fn load() -> Result> { 64 | let mut path = env::current_dir()?; 65 | let file = Path::new("network.toml"); 66 | 67 | let found_path = loop { 68 | path.push(file); 69 | 70 | if path.is_file() { 71 | break path; 72 | } 73 | 74 | if !(path.pop() && path.pop()) { 75 | return Ok(None); 76 | } 77 | }; 78 | 79 | Ok(Some(Self::load_from(&found_path)?)) 80 | } 81 | 82 | pub fn load_from(path: &PathBuf) -> Result { 83 | let data = read_to_string(path)?; 84 | let mut nw: Self = toml::from_str(&data)?; 85 | nw.path = path.parent().unwrap().to_path_buf(); 86 | Ok(nw) 87 | } 88 | 89 | pub fn save(&self) -> Result<()> { 90 | let cfg_str = toml::to_string_pretty(&self)?; 91 | let mut f = File::create(self.path.join("network.toml"))?; 92 | f.write_all(cfg_str.as_bytes())?; 93 | 94 | Ok(()) 95 | } 96 | 97 | pub fn next_port(&self) -> u16 { 98 | let mut port = 25565; 99 | 100 | let mut taken = vec![self.port]; 101 | for serv in self.servers.values() { 102 | taken.push(serv.port); 103 | } 104 | 105 | while taken.contains(&port) { 106 | port += 1; 107 | } 108 | 109 | port 110 | } 111 | } 112 | 113 | impl Default for Network { 114 | fn default() -> Self { 115 | Self { 116 | path: PathBuf::from("."), 117 | name: String::new(), 118 | proxy: "proxy".to_owned(), 119 | proxy_groups: vec![], 120 | port: 25565, 121 | servers: HashMap::new(), 122 | variables: HashMap::new(), 123 | markdown: MarkdownOptions::default(), 124 | hooks: HashMap::new(), 125 | groups: HashMap::new(), 126 | } 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /src/model/servertype/meta.rs: -------------------------------------------------------------------------------- 1 | use indexmap::IndexMap; 2 | 3 | use super::{Downloadable, ServerType}; 4 | use std::borrow::Cow; 5 | 6 | macro_rules! version_id { 7 | ($loader:ident, |$id:ident| $id_format:literal) => { 8 | match $loader.as_str() { 9 | "latest" => "*Latest*".to_owned(), 10 | $id => format!($id_format), 11 | } 12 | }; 13 | 14 | ($loader:ident) => { 15 | version_id!($loader, |id| "`{id}`") 16 | }; 17 | } 18 | 19 | impl ServerType { 20 | pub fn get_md_link(&self) -> String { 21 | match self { 22 | Self::Vanilla {} => "Vanilla".to_owned(), 23 | Self::Velocity {} => "[Velocity](https://papermc.io/software/velocity)".to_owned(), 24 | Self::Waterfall {} => "[Waterfall](https://papermc.io/software/waterfall)".to_owned(), 25 | Self::Paper {} => "[Paper](https://papermc.io/software/paper)".to_owned(), 26 | Self::BuildTools { .. } => { 27 | "[BuildTools](https://www.spigotmc.org/wiki/buildtools/)".to_owned() 28 | } 29 | Self::BungeeCord {} => { 30 | "[BungeeCord](https://www.spigotmc.org/wiki/bungeecord/)".to_owned() 31 | } 32 | Self::Fabric { .. } => "[Fabric](https://fabricmc.net/)".to_owned(), 33 | Self::Purpur { .. } => "[Purpur](https://github.com/PurpurMC/Purpur)".to_owned(), 34 | Self::PaperMC { project, build } => { 35 | format!("[PaperMC/{project}](https://github.com/PaperMC/{project}); build {build}") 36 | } 37 | Self::Quilt { .. } => "[Quilt](https://quiltmc.org/)".to_owned(), 38 | Self::NeoForge { .. } => "[NeoForge](https://neoforged.net/)".to_owned(), 39 | Self::Forge { .. } => "[Forge](https://forums.minecraftforge.net/)".to_owned(), 40 | Self::Downloadable { inner } => inner.get_md_link(), 41 | } 42 | } 43 | 44 | pub fn get_metadata(&self) -> IndexMap, String> { 45 | let mut map = IndexMap::new(); 46 | 47 | match self { 48 | Self::Fabric { loader, installer } | Self::Quilt { loader, installer } => { 49 | map.insert(Cow::Borrowed("Loader"), version_id!(loader)); 50 | 51 | if installer != "latest" { 52 | map.insert(Cow::Borrowed("Installer"), format!("`{installer}`")); 53 | } 54 | } 55 | 56 | Self::NeoForge { loader } | Self::Forge { loader } => { 57 | map.insert(Cow::Borrowed("Loader"), version_id!(loader)); 58 | } 59 | 60 | Self::PaperMC { build, .. } | Self::Purpur { build } => { 61 | map.insert(Cow::Borrowed("Build"), version_id!(build, |id| "`#{id}`")); 62 | } 63 | 64 | Self::Downloadable { inner } => match inner { 65 | Downloadable::Jenkins { 66 | build, artifact, .. 67 | } => { 68 | map.insert(Cow::Borrowed("Build"), version_id!(build, |id| "`#{id}`")); 69 | 70 | if artifact != "first" { 71 | map.insert(Cow::Borrowed("Artifact"), format!("`{artifact}`")); 72 | } 73 | } 74 | 75 | Downloadable::GithubRelease { tag, asset, .. } => { 76 | map.insert(Cow::Borrowed("Release"), version_id!(tag)); 77 | 78 | if asset != "first" { 79 | map.insert(Cow::Borrowed("Asset"), format!("`{asset}`")); 80 | } 81 | } 82 | 83 | _ => {} 84 | }, 85 | 86 | _ => {} 87 | } 88 | 89 | map 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/model/servertype/parse.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Deserializer, Serialize, Serializer}; 2 | 3 | use crate::model::Downloadable; 4 | 5 | use super::ServerType; 6 | 7 | #[derive(Debug, Serialize, Deserialize, Clone)] 8 | #[serde(untagged)] 9 | pub enum Bridge { 10 | ServerType(ServerType), 11 | Downloadable(Downloadable), 12 | } 13 | 14 | impl From for ServerType { 15 | fn from(value: Bridge) -> Self { 16 | match value { 17 | Bridge::ServerType(ty) => ty, 18 | Bridge::Downloadable(d) => Self::Downloadable { inner: d }, 19 | } 20 | } 21 | } 22 | 23 | pub fn serialize(st: &ServerType, serializer: S) -> Result 24 | where 25 | S: Serializer, 26 | { 27 | st.serialize(serializer) 28 | } 29 | 30 | pub fn deserialize<'de, D>(deserializer: D) -> Result 31 | where 32 | D: Deserializer<'de>, 33 | { 34 | Ok(ServerType::from(Bridge::deserialize(deserializer)?)) 35 | } 36 | -------------------------------------------------------------------------------- /src/model/world.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | 3 | use super::Downloadable; 4 | 5 | #[derive(Debug, Deserialize, Serialize, Default, Clone, PartialEq)] 6 | #[serde(default)] 7 | pub struct World { 8 | #[serde(skip_serializing_if = "Vec::is_empty")] 9 | pub datapacks: Vec, 10 | pub download: Option, 11 | } 12 | -------------------------------------------------------------------------------- /src/sources/fabric.rs: -------------------------------------------------------------------------------- 1 | use std::{borrow::Cow, collections::HashMap}; 2 | 3 | use anyhow::{anyhow, Result}; 4 | use mcapi::fabric::{FabricInstaller, FabricLoader, FABRIC_META_URL}; 5 | 6 | use crate::app::{App, CacheStrategy, ResolvedFile}; 7 | 8 | pub struct FabricAPI<'a>(pub &'a App); 9 | 10 | impl<'a> FabricAPI<'a> { 11 | pub async fn fetch_loaders(&self) -> Result> { 12 | Ok(mcapi::fabric::fetch_loaders(&self.0.http_client).await?) 13 | } 14 | 15 | pub async fn fetch_latest_loader(&self) -> Result { 16 | Ok(self 17 | .fetch_loaders() 18 | .await? 19 | .first() 20 | .ok_or(anyhow!("No fabric loaders???"))? 21 | .version 22 | .clone()) 23 | } 24 | 25 | pub async fn fetch_installers(&self) -> Result> { 26 | Ok(mcapi::fabric::fetch_installers(&self.0.http_client).await?) 27 | } 28 | 29 | pub async fn fetch_latest_installer(&self) -> Result { 30 | Ok(self 31 | .fetch_installers() 32 | .await? 33 | .first() 34 | .ok_or(anyhow!("No fabric installers???"))? 35 | .version 36 | .clone()) 37 | } 38 | 39 | pub async fn resolve_source(&self, loader: &str, installer: &str) -> Result { 40 | let loader = match loader { 41 | "latest" => self.fetch_latest_loader().await?, 42 | id => id.to_owned(), 43 | }; 44 | 45 | let installer = match installer { 46 | "latest" => self.fetch_latest_installer().await?, 47 | id => id.to_owned(), 48 | }; 49 | 50 | let cached_file_path = format!( 51 | "fabric-server-{}-{installer}-{loader}.jar", 52 | self.0.mc_version() 53 | ); 54 | 55 | Ok(ResolvedFile { 56 | url: format!( 57 | "{FABRIC_META_URL}/v2/versions/loader/{}/{loader}/{installer}/server/jar", 58 | self.0.mc_version() 59 | ), 60 | filename: cached_file_path.clone(), 61 | cache: CacheStrategy::File { 62 | namespace: Cow::Borrowed("fabric"), 63 | path: cached_file_path, 64 | }, 65 | size: None, 66 | hashes: HashMap::new(), 67 | }) 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/sources/forge.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{anyhow, Context, Result}; 2 | 3 | use crate::app::{App, ResolvedFile}; 4 | 5 | pub static FORGE_MAVEN: &str = "https://maven.minecraftforge.net"; 6 | pub static FORGE_GROUP: &str = "net.minecraftforge"; 7 | pub static FORGE_ARTIFACT: &str = "forge"; 8 | pub static FORGE_FILENAME: &str = "${artifact}-${version}-installer.jar"; 9 | 10 | pub struct ForgeAPI<'a>(pub &'a App); 11 | 12 | impl<'a> ForgeAPI<'a> { 13 | /// Returns a string list of filtered versions, everything after the first dash (-) 14 | pub async fn fetch_versions(&self) -> Result> { 15 | let (_, versions) = self 16 | .0 17 | .maven() 18 | .fetch_versions(FORGE_MAVEN, FORGE_GROUP, FORGE_ARTIFACT) 19 | .await?; 20 | 21 | Ok(versions 22 | .iter() 23 | .filter_map(|s| { 24 | let v = s.split('-').collect::>(); 25 | 26 | if v[0] == self.0.mc_version() { 27 | Some(v[1].to_owned()) 28 | } else { 29 | None 30 | } 31 | }) 32 | .collect()) 33 | } 34 | 35 | pub async fn fetch_latest(&self) -> Result { 36 | crate::util::get_latest_semver(&self.fetch_versions().await?).ok_or(anyhow!( 37 | "No forge loader versions for {}", 38 | self.0.mc_version() 39 | )) 40 | } 41 | 42 | pub async fn resolve_version(&self, loader: &str) -> Result { 43 | Ok(if loader == "latest" || loader.is_empty() { 44 | self.fetch_latest() 45 | .await 46 | .context("Getting latest Forge version")? 47 | } else { 48 | loader.to_owned() 49 | }) 50 | } 51 | 52 | pub async fn resolve_source(&self, loader: &str) -> Result { 53 | self.0 54 | .maven() 55 | .resolve_source( 56 | FORGE_MAVEN, 57 | FORGE_GROUP, 58 | FORGE_ARTIFACT, 59 | &format!( 60 | "{}-{}", 61 | self.0.mc_version(), 62 | self.resolve_version(loader).await? 63 | ), 64 | FORGE_FILENAME, 65 | ) 66 | .await 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/sources/mclogs.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | 3 | use anyhow::{anyhow, Result}; 4 | use serde::{Deserialize, Serialize}; 5 | 6 | use crate::app::App; 7 | 8 | const API_V1: &str = "https://api.mclo.gs/1"; 9 | 10 | pub struct MCLogsAPI<'a>(pub &'a App); 11 | 12 | impl<'a> MCLogsAPI<'a> { 13 | pub async fn paste_log(&self, content: &str) -> Result { 14 | let params = HashMap::from([("content", content)]); 15 | 16 | let json = self 17 | .0 18 | .http_client 19 | .post(format!("{API_V1}/log")) 20 | .form(¶ms) 21 | .send() 22 | .await? 23 | .error_for_status()? 24 | .json::>() 25 | .await?; 26 | 27 | json.into() 28 | } 29 | 30 | #[allow(unused)] 31 | pub async fn fetch_insights(&self, id: &str) -> Result { 32 | let json = self 33 | .0 34 | .http_client 35 | .post(format!("{API_V1}/insights/{id}")) 36 | .send() 37 | .await? 38 | .error_for_status()? 39 | .json::>() 40 | .await?; 41 | 42 | json.into() 43 | } 44 | } 45 | 46 | #[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] 47 | #[serde(untagged)] 48 | pub enum MaybeSuccess { 49 | Error { 50 | error: String, 51 | }, 52 | Success { 53 | #[serde(flatten)] 54 | value: T, 55 | }, 56 | } 57 | 58 | impl From> for Result { 59 | fn from(val: MaybeSuccess) -> Self { 60 | match val { 61 | MaybeSuccess::Success { value } => Ok(value), 62 | MaybeSuccess::Error { error } => Err(anyhow!(error)), 63 | } 64 | } 65 | } 66 | 67 | #[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] 68 | pub struct LogFileMetadata { 69 | pub id: String, 70 | pub url: String, 71 | pub raw: String, 72 | } 73 | 74 | #[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] 75 | pub struct LogInsights { 76 | pub id: String, 77 | pub name: String, 78 | #[serde(rename = "type")] 79 | pub log_type: String, 80 | pub version: String, 81 | pub title: String, 82 | pub analysis: LogAnalysis, 83 | } 84 | 85 | #[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] 86 | pub struct LogAnalysis { 87 | pub problems: Vec, 88 | pub information: Vec, 89 | } 90 | 91 | #[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] 92 | pub struct Problem { 93 | pub message: String, 94 | pub counter: usize, 95 | pub entry: AnalysisEntry, 96 | pub solutions: Vec, 97 | } 98 | 99 | #[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] 100 | pub struct Information { 101 | pub message: String, 102 | pub counter: usize, 103 | pub label: String, 104 | pub value: String, 105 | pub entry: AnalysisEntry, 106 | } 107 | 108 | #[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] 109 | pub struct AnalysisEntry { 110 | pub level: usize, 111 | pub time: Option, 112 | pub prefix: String, 113 | pub lines: Vec, 114 | } 115 | 116 | #[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] 117 | pub struct AnalysisLine { 118 | pub number: usize, 119 | pub content: String, 120 | } 121 | 122 | #[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] 123 | pub struct Solution { 124 | pub message: String, 125 | } 126 | -------------------------------------------------------------------------------- /src/sources/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod curserinth; 2 | pub mod fabric; 3 | pub mod forge; 4 | pub mod github; 5 | pub mod hangar; 6 | pub mod jenkins; 7 | pub mod maven; 8 | pub mod mclogs; 9 | pub mod modrinth; 10 | pub mod neoforge; 11 | pub mod papermc; 12 | pub mod purpur; 13 | pub mod quilt; 14 | pub mod spigot; 15 | pub mod vanilla; 16 | -------------------------------------------------------------------------------- /src/sources/neoforge.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{anyhow, Context, Result}; 2 | 3 | use crate::{app::App, app::ResolvedFile, util}; 4 | 5 | pub static NEOFORGE_MAVEN: &str = "https://maven.neoforged.net/releases"; 6 | pub static NEOFORGE_GROUP: &str = "net.neoforged"; 7 | pub static NEOFORGE_ARTIFACT: &str = "forge"; 8 | pub static NEOFORGE_FILENAME: &str = "${artifact}-${version}-installer.jar"; 9 | 10 | pub struct NeoforgeAPI<'a>(pub &'a App); 11 | 12 | impl<'a> NeoforgeAPI<'a> { 13 | pub async fn fetch_versions(&self) -> Result> { 14 | let (_, versions) = self 15 | .0 16 | .maven() 17 | .fetch_versions(NEOFORGE_MAVEN, NEOFORGE_GROUP, NEOFORGE_ARTIFACT) 18 | .await?; 19 | 20 | Ok(versions 21 | .iter() 22 | .filter_map(|s| { 23 | let (m, l) = s.split_once('-')?; 24 | 25 | if m == self.0.mc_version() { 26 | Some(l.to_owned()) 27 | } else { 28 | None 29 | } 30 | }) 31 | .collect()) 32 | } 33 | 34 | pub async fn fetch_latest(&self) -> Result { 35 | util::get_latest_semver(&self.fetch_versions().await?).ok_or(anyhow!( 36 | "No forge loader versions for {}", 37 | self.0.mc_version() 38 | )) 39 | } 40 | 41 | pub async fn resolve_version(&self, loader: &str) -> Result { 42 | Ok(if loader == "latest" || loader.is_empty() { 43 | self.fetch_latest() 44 | .await 45 | .context("Getting latest Forge version")? 46 | } else { 47 | loader.to_owned() 48 | }) 49 | } 50 | 51 | pub async fn resolve_source(&self, loader: &str) -> Result { 52 | self.0 53 | .maven() 54 | .resolve_source( 55 | NEOFORGE_MAVEN, 56 | NEOFORGE_GROUP, 57 | NEOFORGE_ARTIFACT, 58 | &format!( 59 | "{}-{}", 60 | self.0.mc_version(), 61 | self.resolve_version(loader).await? 62 | ), 63 | NEOFORGE_FILENAME, 64 | ) 65 | .await 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/sources/papermc.rs: -------------------------------------------------------------------------------- 1 | use std::{borrow::Cow, collections::HashMap}; 2 | 3 | use anyhow::{anyhow, Result}; 4 | use mcapi::papermc::{PaperBuildsResponse, PaperProject}; 5 | use serde::{de::DeserializeOwned, Serialize}; 6 | 7 | use crate::app::{App, CacheStrategy, ResolvedFile}; 8 | 9 | pub struct PaperMCAPI<'a>(pub &'a App); 10 | 11 | const PAPERMC_URL: &str = "https://api.papermc.io/v2"; 12 | const CACHE_DIR: &str = "papermc"; 13 | 14 | impl<'a> PaperMCAPI<'a> { 15 | pub async fn fetch_api( 16 | &self, 17 | url: String, 18 | ) -> Result { 19 | let response = self.0.http_client.get(&url).send().await?; 20 | 21 | let json: T = response.error_for_status()?.json().await?; 22 | 23 | Ok(json) 24 | } 25 | 26 | pub async fn fetch_versions(&self, project: &str) -> Result> { 27 | let proj = self 28 | .fetch_api::(format!("{PAPERMC_URL}/projects/{project}")) 29 | .await?; 30 | 31 | Ok(proj.versions) 32 | } 33 | 34 | pub async fn fetch_builds(&self, project: &str, version: &str) -> Result { 35 | let resp = self 36 | .fetch_api(format!( 37 | "{PAPERMC_URL}/projects/{project}/versions/{version}/builds" 38 | )) 39 | .await?; 40 | 41 | Ok(resp) 42 | } 43 | 44 | pub async fn fetch_build( 45 | &self, 46 | project: &str, 47 | version: &str, 48 | build: &str, 49 | ) -> Result { 50 | let builds = self.fetch_builds(project, version).await?; 51 | Ok(match build { 52 | "latest" => builds 53 | .builds 54 | .last() 55 | .ok_or(anyhow!( 56 | "Latest papermc build for project {project} {version} not found" 57 | ))? 58 | .clone(), 59 | id => builds 60 | .builds 61 | .iter() 62 | .find(|&b| b.build.to_string() == id) 63 | .ok_or(anyhow!( 64 | "PaperMC build '{build}' for project {project} {version} not found" 65 | ))? 66 | .clone(), 67 | }) 68 | } 69 | 70 | pub async fn resolve_source( 71 | &self, 72 | project: &str, 73 | version: &str, 74 | build: &str, 75 | ) -> Result { 76 | let version = match version { 77 | "latest" => self 78 | .fetch_versions(project) 79 | .await? 80 | .last() 81 | .ok_or(anyhow!("No versions"))? 82 | .clone(), 83 | id => id.to_owned(), 84 | }; 85 | 86 | let resolved_build = self.fetch_build(project, &version, build).await?; 87 | 88 | let download = resolved_build.downloads.get("application") 89 | .ok_or(anyhow!("downloads['application'] missing for papermc project {project} {version}, build {build} ({})", resolved_build.build))?; 90 | let cached_file_path = format!("{project}/{}", download.name); 91 | 92 | Ok(ResolvedFile { 93 | url: format!( 94 | "{PAPERMC_URL}/projects/{project}/versions/{version}/builds/{}/downloads/{}", 95 | resolved_build.build, download.name 96 | ), 97 | filename: download.name.clone(), 98 | cache: CacheStrategy::File { 99 | namespace: Cow::Borrowed(CACHE_DIR), 100 | path: cached_file_path, 101 | }, 102 | size: None, 103 | hashes: HashMap::new(), 104 | }) 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /src/sources/purpur.rs: -------------------------------------------------------------------------------- 1 | use std::{borrow::Cow, collections::HashMap}; 2 | 3 | use anyhow::{anyhow, Result}; 4 | use serde::{de::DeserializeOwned, Deserialize, Serialize}; 5 | 6 | use crate::app::{App, CacheStrategy, ResolvedFile}; 7 | 8 | pub struct PurpurAPI<'a>(pub &'a App); 9 | 10 | pub const API_URL: &str = "https://api.purpurmc.org/v2/purpur"; 11 | pub const CACHE_DIR: &str = "purpur"; 12 | 13 | impl<'a> PurpurAPI<'a> { 14 | pub async fn fetch_api(&self, url: &str) -> Result { 15 | let response: T = self 16 | .0 17 | .http_client 18 | .get(url) 19 | .send() 20 | .await? 21 | .error_for_status()? 22 | .json() 23 | .await?; 24 | 25 | Ok(response) 26 | } 27 | 28 | #[allow(unused)] 29 | pub async fn fetch_versions(&self) -> Result> { 30 | Ok(self.fetch_api::(API_URL).await?.versions) 31 | } 32 | 33 | pub async fn fetch_builds(&self, version: &str) -> Result { 34 | Ok(self 35 | .fetch_api::(&format!("{API_URL}/{version}?detailed=true")) 36 | .await? 37 | .builds) 38 | } 39 | 40 | pub async fn fetch_build(&self, version: &str, build: &str) -> Result { 41 | let builds = self.fetch_builds(version).await?; 42 | 43 | Ok(match build { 44 | "latest" => builds.latest.clone(), 45 | id => builds 46 | .all 47 | .iter() 48 | .find(|b| b.build == id) 49 | .ok_or(anyhow!("Cant find build '{id}' of Purpur {version}"))? 50 | .clone(), 51 | }) 52 | } 53 | 54 | pub async fn resolve_source(&self, version: &str, build: &str) -> Result { 55 | let resolved_build = self.fetch_build(version, build).await?; 56 | 57 | let cached_file_path = format!("purpur-{version}-{}.jar", resolved_build.build); 58 | 59 | Ok(ResolvedFile { 60 | url: format!("{API_URL}/{version}/{}/download", resolved_build.build), 61 | filename: cached_file_path.clone(), 62 | cache: CacheStrategy::File { 63 | namespace: Cow::Borrowed(CACHE_DIR), 64 | path: cached_file_path, 65 | }, 66 | size: None, 67 | hashes: HashMap::from([("md5".to_owned(), resolved_build.md5)]), 68 | }) 69 | } 70 | } 71 | 72 | #[derive(Debug, Deserialize, Serialize, Clone)] 73 | pub struct PurpurMCResponse { 74 | pub project: String, 75 | pub versions: Vec, 76 | } 77 | 78 | #[derive(Debug, Deserialize, Serialize, Clone)] 79 | pub struct PurpurMCVersion { 80 | pub builds: PurpurMCBuilds, 81 | pub project: String, 82 | pub version: String, 83 | } 84 | 85 | #[derive(Debug, Deserialize, Serialize, Clone)] 86 | pub struct PurpurMCBuilds { 87 | pub latest: PurpurMCBuild, 88 | pub all: Vec, 89 | } 90 | 91 | #[derive(Debug, Deserialize, Serialize, Clone)] 92 | pub struct PurpurMCBuild { 93 | pub project: String, 94 | pub version: String, 95 | pub build: String, 96 | pub md5: String, 97 | } 98 | -------------------------------------------------------------------------------- /src/sources/quilt.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{anyhow, Result}; 2 | 3 | use crate::app::{App, ResolvedFile}; 4 | 5 | pub struct QuiltAPI<'a>(pub &'a App); 6 | 7 | pub const QUILT_MAVEN_URL: &str = "https://maven.quiltmc.org/repository/release"; 8 | pub const QUILT_MAVEN_GROUP: &str = "org.quiltmc"; 9 | pub const QUILT_MAVEN_ARTIFACT: &str = "quilt-installer"; 10 | pub const QUILT_MAVEN_FILE: &str = "${artifact}-${version}.jar"; 11 | 12 | impl<'a> QuiltAPI<'a> { 13 | pub async fn resolve_installer(&self, version: &str) -> Result { 14 | self.0 15 | .maven() 16 | .resolve_source( 17 | QUILT_MAVEN_URL, 18 | QUILT_MAVEN_GROUP, 19 | QUILT_MAVEN_ARTIFACT, 20 | version, 21 | QUILT_MAVEN_FILE, 22 | ) 23 | .await 24 | } 25 | } 26 | 27 | pub async fn map_quilt_loader_version(client: &reqwest::Client, loader: &str) -> Result { 28 | Ok(match loader { 29 | "latest" => mcapi::quilt::fetch_loaders(client) 30 | .await? 31 | .iter() 32 | .find(|l| !l.version.contains("beta")) 33 | .ok_or(anyhow!("cant find latest loader version - None"))? 34 | .version 35 | .clone(), 36 | "latest-beta" => mcapi::quilt::fetch_loaders(client) 37 | .await? 38 | .first() 39 | .ok_or(anyhow!("cant find latest loader version - None"))? 40 | .version 41 | .clone(), 42 | id => id.to_owned(), 43 | }) 44 | } 45 | -------------------------------------------------------------------------------- /src/sources/spigot.rs: -------------------------------------------------------------------------------- 1 | use std::{borrow::Cow, collections::HashMap}; 2 | 3 | use anyhow::Result; 4 | use serde::{de::DeserializeOwned, Deserialize, Serialize}; 5 | 6 | use crate::app::{App, CacheStrategy, ResolvedFile}; 7 | 8 | #[derive(Debug, Deserialize, Serialize)] 9 | struct SpigotResourceVersion { 10 | pub name: String, 11 | pub id: i32, 12 | } 13 | 14 | pub struct SpigotAPI<'a>(pub &'a App); 15 | 16 | pub const API_URL: &str = "https://api.spiget.org/v2"; 17 | pub const CACHE_DIR: &str = "spiget"; 18 | 19 | impl<'a> SpigotAPI<'a> { 20 | pub async fn fetch_api(&self, url: &str) -> Result { 21 | let response: T = self 22 | .0 23 | .http_client 24 | .get(url) 25 | .send() 26 | .await? 27 | .error_for_status()? 28 | .json() 29 | .await?; 30 | 31 | Ok(response) 32 | } 33 | 34 | pub fn get_resource_id(res: &str) -> &str { 35 | if let Some(i) = res.find('.') { 36 | if i < res.len() - 1 { 37 | return res.split_at(i + 1).1; 38 | } 39 | } 40 | 41 | res 42 | } 43 | 44 | pub async fn fetch_info(&self, id: &str) -> Result<(String, String)> { 45 | let json = self 46 | .fetch_api::(&format!( 47 | "{API_URL}/resources/{}", 48 | Self::get_resource_id(id) 49 | )) 50 | .await?; 51 | 52 | Ok(( 53 | json["name"].as_str().unwrap().to_owned(), 54 | json["tag"].as_str().unwrap().to_owned(), 55 | )) 56 | } 57 | 58 | #[allow(unused)] 59 | pub async fn fetch_versions(&self, id: &str) -> Result> { 60 | self.fetch_api(&format!( 61 | "{API_URL}/resources/{}/versions", 62 | Self::get_resource_id(id) 63 | )) 64 | .await 65 | } 66 | 67 | pub async fn fetch_version(&self, id: &str, version: &str) -> Result { 68 | self.fetch_api(&format!( 69 | "{API_URL}/resources/{}/versions/{version}", 70 | Self::get_resource_id(id) 71 | )) 72 | .await 73 | } 74 | 75 | pub async fn resolve_source(&self, id: &str, version: &str) -> Result { 76 | let resolved_version = self.fetch_version(id, version).await?; 77 | 78 | let filename = format!("spigot-{id}-{}.jar", resolved_version.name); 79 | let cached_file_path = format!("{id}/{}.jar", resolved_version.id); 80 | 81 | Ok(ResolvedFile { 82 | url: format!( 83 | "{API_URL}/resources/{}/versions/{version}/download/proxy", 84 | Self::get_resource_id(id) 85 | ), 86 | filename, 87 | cache: CacheStrategy::File { 88 | namespace: Cow::Borrowed(CACHE_DIR), 89 | path: cached_file_path, 90 | }, 91 | size: None, 92 | hashes: HashMap::new(), 93 | }) 94 | } 95 | } 96 | 97 | #[derive(Debug, Serialize, Deserialize, Clone)] 98 | pub struct SpigotVersion { 99 | pub uuid: String, 100 | pub name: String, 101 | pub resource: i64, 102 | pub id: i64, 103 | } 104 | -------------------------------------------------------------------------------- /src/sources/vanilla.rs: -------------------------------------------------------------------------------- 1 | use crate::app::{App, CacheStrategy, ResolvedFile}; 2 | use anyhow::{anyhow, Context, Result}; 3 | use std::{borrow::Cow, collections::HashMap}; 4 | 5 | pub struct VanillaAPI<'a>(pub &'a App); 6 | 7 | pub const CACHE_DIR: &str = "vanilla"; 8 | 9 | impl<'a> VanillaAPI<'a> { 10 | pub async fn fetch_latest_mcver(&self) -> Result { 11 | Ok(mcapi::vanilla::fetch_version_manifest(&self.0.http_client) 12 | .await? 13 | .latest 14 | .release) 15 | } 16 | 17 | pub async fn resolve_source(&self, version: &str) -> Result { 18 | let version_manifest = mcapi::vanilla::fetch_version_manifest(&self.0.http_client) 19 | .await 20 | .context("Fetching version manifest")?; 21 | 22 | let version = match version { 23 | "latest" => version_manifest 24 | .fetch_latest_release(&self.0.http_client) 25 | .await 26 | .context("Fetching latest release")?, 27 | "latest-snapshot" => version_manifest 28 | .fetch_latest_snapshot(&self.0.http_client) 29 | .await 30 | .context("Fetching latest snapshot")?, 31 | id => version_manifest 32 | .fetch(id, &self.0.http_client) 33 | .await 34 | .context(format!("Fetching release {id}"))?, 35 | }; 36 | 37 | let file = version 38 | .downloads 39 | .get(&mcapi::vanilla::DownloadType::Server) 40 | .ok_or(anyhow!( 41 | "version manifest doesn't include a server download" 42 | ))?; 43 | 44 | let cached_file_path = format!("server-{}.jar", version.id); 45 | 46 | Ok(ResolvedFile { 47 | url: file.url.clone(), 48 | filename: cached_file_path.clone(), 49 | cache: CacheStrategy::File { 50 | namespace: Cow::Borrowed(CACHE_DIR), 51 | path: cached_file_path, 52 | }, 53 | size: Some(file.size as u64), 54 | hashes: HashMap::from([("sha1".to_owned(), file.sha1.clone())]), 55 | }) 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/util/maven_import.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{anyhow, bail, Result}; 2 | 3 | use crate::model::Downloadable; 4 | 5 | macro_rules! dependency { 6 | ($xml:ident, $name:literal) => { 7 | $xml.descendants() 8 | .find(|t| t.tag_name().name() == $name) 9 | .ok_or(anyhow!(concat!("dependency.", $name, " must be present")))? 10 | .text() 11 | .ok_or(anyhow!(concat!("dependency.", $name, " must be text")))? 12 | .to_owned() 13 | }; 14 | } 15 | 16 | /// Example: 17 | /// ```xml 18 | /// 19 | /// net.neoforged 20 | /// forge 21 | /// 1.20.1-47.1.62 22 | /// 23 | /// ``` 24 | #[allow(unused)] 25 | pub fn import_from_maven_dependency_xml(url: &str, xml: &str) -> Result { 26 | let xml = roxmltree::Document::parse(xml)?; 27 | 28 | let group = dependency!(xml, "groupId"); 29 | let artifact = dependency!(xml, "artifactId"); 30 | let version = dependency!(xml, "version"); 31 | 32 | Ok(Downloadable::Maven { 33 | url: url.to_owned(), 34 | artifact, 35 | group, 36 | version, 37 | filename: "${artifact}-${version}".to_owned(), 38 | }) 39 | } 40 | 41 | /// Gradle Kotlin: 42 | /// ``` 43 | /// implementation("net.neoforged:forge:1.20.1-47.1.62") 44 | /// ``` 45 | /// 46 | /// Gradle Groovy: 47 | /// 48 | /// ``` 49 | /// implementation "net.neoforged:forge:1.20.1-47.1.62" 50 | /// ``` 51 | #[allow(unused)] 52 | pub fn import_from_gradle_dependency(url: &str, imp: &str) -> Result { 53 | let imp = imp 54 | .replace("implementation", "") 55 | .replace([' ', '(', ')', '"'], ""); 56 | let li = imp.trim().split(':').collect::>(); 57 | 58 | if li.len() != 3 { 59 | bail!("Gradle dependency should have 3 sections delimeted by ':' inside the quoted string"); 60 | } 61 | 62 | Ok(Downloadable::Maven { 63 | url: url.to_owned(), 64 | group: li[0].to_owned(), 65 | artifact: li[1].to_owned(), 66 | version: li[2].to_owned(), 67 | filename: "${artifact}-${version}".to_owned(), 68 | }) 69 | } 70 | 71 | /// Example: 72 | /// ``` 73 | /// "net.neoforged" %% "forge" %% "1.20.1-47.1.62" 74 | /// ``` 75 | #[allow(unused)] 76 | pub fn import_from_sbt(url: &str, sbt: &str) -> Result { 77 | let sbt = sbt.replace(char::is_whitespace, "").replace('"', ""); 78 | let li = sbt.split('%').filter(|x| !x.is_empty()).collect::>(); 79 | 80 | if li.len() != 3 { 81 | bail!("SBT should have 3 sections delimeted by '%' or '%%'"); 82 | } 83 | 84 | Ok(Downloadable::Maven { 85 | url: url.to_owned(), 86 | group: li[0].to_owned(), 87 | artifact: li[1].to_owned(), 88 | version: li[2].to_owned(), 89 | filename: "${artifact}-${version}".to_owned(), 90 | }) 91 | } 92 | -------------------------------------------------------------------------------- /src/util/mod.rs: -------------------------------------------------------------------------------- 1 | use std::{borrow::Cow, cmp::Ordering}; 2 | 3 | use anyhow::Result; 4 | use regex::Regex; 5 | 6 | pub mod env; 7 | pub mod maven_import; 8 | pub mod md; 9 | 10 | pub struct SelectItem(pub T, pub Cow<'static, str>); 11 | 12 | impl ToString for SelectItem { 13 | fn to_string(&self) -> String { 14 | match &self.1 { 15 | Cow::Borrowed(s) => (*s).to_string(), 16 | Cow::Owned(s) => s.clone(), 17 | } 18 | } 19 | } 20 | 21 | pub fn is_default(t: &T) -> bool { 22 | t == &T::default() 23 | } 24 | 25 | pub fn is_default_str(s: &str) -> bool { 26 | s == "latest" 27 | } 28 | 29 | pub fn get_latest_semver(list: &[String]) -> Option { 30 | let mut list = list 31 | .iter() 32 | .map(|s| s.split('.').collect()) 33 | .collect::>>(); 34 | 35 | list.sort_by(|a, b| { 36 | let mut ia = a.iter(); 37 | let mut ib = b.iter(); 38 | loop { 39 | break match (ia.next(), ib.next()) { 40 | (Some(a), Some(b)) => { 41 | let a = a.parse::(); 42 | let b = b.parse::(); 43 | 44 | match (a, b) { 45 | (Ok(a), Ok(b)) => match a.cmp(&b) { 46 | Ordering::Equal => continue, 47 | ord => ord, 48 | }, 49 | (Err(_), Ok(_)) => Ordering::Less, 50 | (Ok(_), Err(_)) => Ordering::Greater, 51 | _ => Ordering::Equal, 52 | } 53 | } 54 | (Some(_), None) => Ordering::Greater, 55 | (None, Some(_)) => Ordering::Less, 56 | _ => Ordering::Equal, 57 | }; 58 | } 59 | }); 60 | 61 | list.last().map(|v| v.join(".")) 62 | } 63 | 64 | /// ci.luckto.me => ci-lucko-me 65 | pub fn url_to_folder(url: &str) -> String { 66 | url.replace("https://", "") 67 | .replace("http://", "") 68 | .replace('/', " ") 69 | .trim() 70 | .replace(' ', "-") 71 | } 72 | 73 | static SANITIZE_R1: &str = "<(?:\"[^\"]*\"['\"]*|'[^']*'['\"]*|[^'\">])+>"; 74 | 75 | pub fn sanitize(s: &str) -> Result { 76 | let re = Regex::new(SANITIZE_R1)?; 77 | 78 | Ok(re 79 | .replace_all( 80 | &s.replace('\n', " ").replace('\r', "").replace("
", " "), 81 | "", 82 | ) 83 | .to_string()) 84 | } 85 | --------------------------------------------------------------------------------