Skip to content

Commit 0a49606

Browse files
committed
Added READMEs
1 parent 8315c1c commit 0a49606

File tree

11 files changed

+323
-87
lines changed

11 files changed

+323
-87
lines changed

README.md

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
# Modder-rs
2+
3+
[![CI](https://github.com/jayansunil/modder/actions/workflows/rust.yml/badge.svg)](https://github.com/jayansunil/modder/actions/workflows/rust.yml)
4+
5+
A simple, fast tool for managing Minecraft mods from the command line.
6+
7+
Modder is a tool for managing mods for Minecraft. It can add mods from Modrinth, CurseForge, and Github Releases. Other features include bulk-updating a directory of mods to a specified version, listing detailed information about the mods in a directory, and toggling mods on or off without deleting the files.
8+
9+
## Features
10+
11+
- [x] Bulk-update a directory of mods
12+
- [x] Add mods via Modrinth
13+
- [ ] Add mods via CurseForge (implementation on hold)
14+
- [x] Add mods via Github Releases
15+
- [x] Toggle mods in a directory (enables/disables them by renaming the file extension)
16+
- [x] List mods with details like version, source, and category
17+
- [ ] Support for `modpacks`
18+
19+
## Workspace Structure
20+
21+
This repository is a cargo workspace containing two main crates:
22+
23+
- `core`: The primary crate that contains all the command-line logic, API wrappers, and file management code.
24+
- `tui`: A work-in-progress crate for a Terminal User Interface (TUI) for `modder`.
25+
26+
## Installation
27+
28+
1. Ensure you have Rust and Cargo installed.
29+
2. Clone the repository:
30+
```sh
31+
git clone https://github.com/jayansunil/modder.git
32+
cd modder
33+
```
34+
3. Install the binary:
35+
```sh
36+
cargo install --path .
37+
```
38+
This will install the `modder` binary in your cargo bin path.
39+
40+
## Usage
41+
42+
Modder provides several commands to manage your mods.
43+
44+
### `add`
45+
46+
Add a mod from Modrinth, CurseForge, or GitHub.
47+
48+
```sh
49+
modder add <MOD_NAME> --version <GAME_VERSION> --loader <LOADER>
50+
```
51+
52+
- **Example (Modrinth):**
53+
```sh
54+
modder add sodium --version 1.21 --loader fabric
55+
```
56+
- **Example (GitHub):** If the mod is on GitHub, `modder` will infer it.
57+
```sh
58+
modder add fabricmc/fabric-api --version 1.21
59+
```
60+
- **Example (CurseForge):**
61+
```sh
62+
modder add create --version 1.20.1 --loader forge --source curseforge
63+
```
64+
65+
### `update`
66+
67+
Bulk-update all mods in a directory to a specific game version.
68+
69+
```sh
70+
modder update --dir ./mods --version <NEW_GAME_VERSION>
71+
```
72+
73+
- **Example:**
74+
```sh
75+
modder update --dir ./mods --version 1.21 --delete-previous
76+
```
77+
78+
### `list`
79+
80+
List all mods in a directory with detailed information.
81+
82+
```sh
83+
modder list [--dir ./mods] [--verbose]
84+
```
85+
86+
- **Example:**
87+
```sh
88+
modder list --dir ./mods --verbose
89+
```
90+
91+
### `toggle`
92+
93+
Enable or disable mods in a directory interactively.
94+
95+
```sh
96+
modder toggle [--dir ./mods]
97+
```
98+
99+
### `quick-add`
100+
101+
Interactively select from a list of popular mods to add.
102+
103+
```sh
104+
modder quick-add --version <GAME_VERSION> --loader <LOADER>
105+
```
106+
107+
## License
108+
109+
This project is licensed under the MIT License. See the [LICENSE](tui/LICENSE) file for details.

core/README.md

Lines changed: 29 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,32 @@
1-
# Modder-rs
1+
# Modder Core
22

3-
A simple, fast tool to make managing minecraft mods that much easier
3+
This crate contains the core business logic for the `modder` command-line tool. It handles interactions with modding APIs, file management, and the implementation of the CLI commands.
44

5-
---
6-
# Features
7-
- [x] Bulk-Update a directory of mods
8-
- [x] Add mods via modrinth
9-
- [ ] Add mods via curseforge
5+
## Features
6+
7+
- [x] Bulk-update a directory of mods
8+
- [x] Add mods via Modrinth
9+
- [x] Add mods via CurseForge
1010
- [x] Add mods via Github Releases
11-
- [x] Toggle Mods in a directory
12-
- [x] Edit the `minecraft/mods` directory
13-
- [ ] Support `modpacks`
11+
- [x] Toggle mods in a directory
12+
- [x] List mods with detailed information
13+
- [ ] Support for `modpacks`
14+
15+
## Core Functionality
16+
17+
The crate is structured into several key modules:
18+
19+
- **`modrinth_wrapper`**: Provides functions for interacting with the Modrinth API (`v2`), including searching for mods, fetching version information, and handling dependencies.
20+
- **`curseforge_wrapper`**: Contains the logic for interacting with the CurseForge API. **Note: This implementation is currently on hold due to API complexity.**
21+
- **`gh_releases`**: Handles fetching release information and downloading mod files from GitHub Repositories.
22+
- **`actions`**: Implements the primary logic for each of the CLI subcommands (e.g., `add`, `update`, `list`).
23+
- **`cli`**: Defines the command-line interface structure, arguments, and subcommands using the `clap` crate.
24+
- **`metadata`**: Manages reading and writing custom metadata to mod JAR files. This is used to track the source of a mod (Modrinth, GitHub, etc.) for future updates.
25+
26+
## Usage
27+
28+
While this crate can be used as a library to build other Minecraft-related tools, its primary purpose is to serve as the engine for the `modder` binary. It is not intended for direct use by end-users.
29+
30+
## License
31+
32+
This project is licensed under the MIT License.

core/src/actions.rs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -265,13 +265,12 @@ pub async fn run(cli: Cli) -> color_eyre::Result<()> {
265265
Source::CurseForge => {
266266
let api = CurseForgeAPI::new(API_KEY.to_string());
267267
let dependencies = Arc::new(Mutex::new(Vec::new()));
268-
let mods = api.search_mods(&version, loader, &mod_).await.unwrap();
268+
let mods = api.search_mods(&version, loader, &mod_, 30).await.unwrap();
269269
let prompt = inquire::MultiSelect::new("Select mods", mods);
270270
let selected = prompt.prompt().unwrap();
271271
let mut handles = Vec::new();
272272
let dir = Arc::new(dir.clone());
273273
for mod_ in selected {
274-
dbg!(&mod_.latest_files[0].file_fingerprint);
275274
let dependencies = Arc::clone(&dependencies);
276275
let version = version.clone();
277276
let api = api.clone();

core/src/curseforge_wrapper/mod.rs

Lines changed: 47 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
//!WARN: ON HOLD for now because this API is tom-fucking-beaurocratic shit
21
mod file_utils;
32
mod hash;
43
mod structs;
@@ -181,14 +180,47 @@ impl CurseForgeAPI {
181180
fs::write(&path, file_data.bytes().await?)?;
182181
Ok(())
183182
}
183+
pub async fn get_version_from_file(&self, file: PathBuf) -> Result<File> {
184+
let f = file.clone();
185+
let f_name = f.file_name().unwrap().to_str().unwrap();
186+
let contents = get_jar_contents(&file)?;
187+
let fingerprint = MurmurHash2::hash(&contents);
188+
let url = format!("{BASE_URL}/fingerprints/{GAME_ID}");
189+
let mut headers = HEADERS.clone();
190+
headers.insert(
191+
HeaderName::from_static("content-type"),
192+
HeaderValue::from_static("application/json"),
193+
);
194+
let body = json!({
195+
"fingerprints": [
196+
fingerprint
197+
]});
198+
let body = serde_json::to_string(&body)?;
199+
let response = self
200+
.client
201+
.request(Method::POST, Url::parse(&url)?)
202+
.headers(headers)
203+
.body(body)
204+
.send()
205+
.await?;
206+
let response = response.error_for_status()?;
207+
let body = response.text().await?;
208+
209+
let res: FingerprintResponseRoot = serde_json::from_str(&body)?;
210+
let res = res.data;
211+
if res.exact_matches.is_empty() {
212+
return Err(CurseForgeError::NoFingerprintFound(f_name.to_string()));
213+
}
214+
let exact_match = res.exact_matches.first().unwrap();
215+
let file = exact_match.file.clone();
216+
Ok(file)
217+
}
184218
pub async fn get_mod_from_file(&self, file: PathBuf) -> Result<Mod> {
185219
let f = file.clone();
186220
let f_name = f.file_name().unwrap().to_str().unwrap();
187221
let contents = get_jar_contents(&file)?;
188222
let fingerprint = MurmurHash2::hash(&contents);
189-
dbg!(&fingerprint);
190223
let url = format!("{BASE_URL}/fingerprints/{GAME_ID}");
191-
dbg!(&url);
192224
let mut headers = HEADERS.clone();
193225
headers.insert(
194226
HeaderName::from_static("content-type"),
@@ -208,8 +240,8 @@ impl CurseForgeAPI {
208240
.await?;
209241
let response = response.error_for_status()?;
210242
let body = response.text().await?;
211-
dbg!(&body);
212-
let res: FingerprintResponse = serde_json::from_str(&body)?;
243+
let res: FingerprintResponseRoot = serde_json::from_str(&body)?;
244+
let res = res.data;
213245
if res.exact_matches.is_empty() {
214246
return Err(CurseForgeError::NoFingerprintFound(f_name.to_string()));
215247
}
@@ -254,7 +286,10 @@ mod tests {
254286
async fn test_search_mods() {
255287
let api = CurseForgeAPI::new(API_KEY.to_string());
256288
let loader = ModLoader::Fabric;
257-
let mods = api.search_mods("1.21.4", loader, "Carpet").await.unwrap();
289+
let mods = api
290+
.search_mods("1.21.4", loader, "Carpet", 10)
291+
.await
292+
.unwrap();
258293
assert_eq!(!mods.is_empty(), true);
259294
}
260295
#[tokio::test]
@@ -281,7 +316,7 @@ mod tests {
281316
let loader = ModLoader::Fabric;
282317
let v = "1.21.4";
283318
let api = CurseForgeAPI::new(API_KEY.to_string());
284-
let mods = api.search_mods(v, loader, search).await.unwrap();
319+
let mods = api.search_mods(v, loader, search, 10).await.unwrap();
285320
println!("{:#?}", mods);
286321
let prompt = inquire::MultiSelect::new("Select mods", mods);
287322
let selected = prompt.prompt().unwrap();
@@ -310,10 +345,13 @@ mod tests {
310345
async fn test_fingerprint_specific_jar() {
311346
color_eyre::install().unwrap();
312347
let api = CurseForgeAPI::new(API_KEY.to_string());
313-
let jar_path =
314-
PathBuf::from("/Users/jayansunil/Downloads/fabric-carpet-1.21.7-1.4.177+v250630.jar");
348+
let jar_path = PathBuf::from(
349+
"/Users/jayansunil/Dev/rust/modder/tui/test/createaddition-1.19.2-1.2.3.jar",
350+
);
315351
let fingerprint = MurmurHash2::hash(&get_jar_contents(&jar_path).unwrap());
316352
dbg!(&fingerprint);
317353
// The fingerprint will be debug-printed by dbg!(&fingerprint) inside get_mod_from_file
354+
let mod_ = api.get_mod_from_file(jar_path).await.unwrap();
355+
assert_eq!(mod_.name, "CreateAddition");
318356
}
319357
}

core/src/curseforge_wrapper/structs.rs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -213,6 +213,12 @@ pub struct ModBuilder {
213213
pub search: String,
214214
}
215215

216+
#[derive(Debug, Deserialize, Clone)]
217+
#[serde(rename_all = "camelCase")]
218+
pub struct FingerprintResponseRoot {
219+
pub data: FingerprintResponse,
220+
}
221+
216222
#[derive(Debug, Deserialize, Clone)]
217223
#[serde(rename_all = "camelCase")]
218224
pub struct FingerprintResponse {
@@ -223,7 +229,7 @@ pub struct FingerprintResponse {
223229
pub partial_matches: Vec<PartialMatch>,
224230
pub partial_match_fingerprints: std::collections::HashMap<String, Vec<u32>>,
225231
pub installed_fingerprints: Vec<u32>,
226-
pub unmatched_fingerprints: Vec<u32>,
232+
pub unmatched_fingerprints: Option<Vec<u32>>,
227233
}
228234

229235
#[derive(Debug, Deserialize, Clone)]

core/src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ pub mod modrinth_wrapper;
77
use clap::ValueEnum;
88
use cli::Source;
99
use color_eyre::Result;
10-
use color_eyre::eyre::{ContextCompat, bail};
10+
use color_eyre::eyre::bail;
1111
use curseforge_wrapper::{CurseForgeAPI, CurseForgeMod};
1212
use gh_releases::{Error, GHReleasesAPI};
1313
use hmac_sha512::Hash;

core/src/modrinth_wrapper/modrinth.rs

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,15 @@
11
#![allow(dead_code)]
2-
use crate::cli::Source;
3-
use crate::gh_releases::{self, GHReleasesAPI};
4-
use crate::metadata::{Error as MetadataError, Metadata};
2+
use crate::gh_releases::{self};
3+
use crate::metadata::Error as MetadataError;
54
use crate::{Link, ModLoader, calc_sha512};
6-
use clap::ValueEnum;
7-
use color_eyre::eyre::{self, ContextCompat, bail, eyre};
5+
use color_eyre::eyre::{ContextCompat};
86
use colored::Colorize;
97
use futures::lock::Mutex;
108
use itertools::Itertools;
119
use serde::{Deserialize, Serialize};
12-
use std::path::PathBuf;
13-
use std::rc::Rc;
1410
use std::sync::Arc;
1511
use std::{fmt::Display, fs};
16-
use tracing::{self, debug, error, info, warn};
12+
use tracing::{self, debug, error, info};
1713

1814
#[derive(thiserror::Error, Debug)]
1915
pub enum Error {

tui/src/components/add.rs

Lines changed: 32 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ use futures::{executor::block_on, lock::Mutex};
66
use modder::{
77
MOD_LOADERS, ModLoader, calc_sha512,
88
cli::{SOURCES, Source},
9-
curseforge_wrapper::{CurseForgeAPI, CurseForgeError},
9+
curseforge_wrapper::{API_KEY, CurseForgeAPI},
1010
gh_releases::{GHReleasesAPI, get_mod_from_release},
1111
metadata::Metadata,
1212
modrinth_wrapper::modrinth::{self, GetProject, Mod, Modrinth, VersionData},
@@ -1304,23 +1304,40 @@ async fn get_mods(dir: PathBuf) -> Vec<CurrentModsListItem> {
13041304
let enabled = !path_str.contains("disabled");
13051305
let version_data = VersionData::from_hash(hash).await;
13061306
if version_data.is_err() {
1307-
let metadata = Metadata::get_all_metadata(path_str.clone().into());
1308-
if metadata.is_err() {
1309-
error!(version_data = ?version_data, "Failed to get version data for {}", path_str);
1310-
return None;
1307+
debug!(path = ?path);
1308+
let cf = CurseForgeAPI::new(API_KEY.to_string());
1309+
let mod_ = cf.get_mod_from_file(path.clone()).await;
1310+
if mod_.is_err() {
1311+
debug!(path = ?path);
1312+
let metadata = Metadata::get_all_metadata(path_str.clone().into());
1313+
if metadata.is_err() {
1314+
error!(version_data = ?version_data, "Failed to get version data for {}", path_str);
1315+
return None;
1316+
}
1317+
let metadata = metadata.unwrap();
1318+
let source = metadata.get("source").unwrap();
1319+
if source.is_empty() {
1320+
error!(version_data = ?version_data, "Failed to get version data for {}", path_str);
1321+
return None;
1322+
}
1323+
let repo = metadata.get("repo").unwrap();
1324+
let repo_name = repo.split('/').last().unwrap();
1325+
let out = CurrentModsListItem {
1326+
name: repo_name.to_string(),
1327+
version_type: "GITHUB".to_string(),
1328+
project_id: repo.to_string(),
1329+
enabled,
1330+
};
1331+
return Some(out);
13111332
}
1312-
let metadata = metadata.unwrap();
1313-
let source = metadata.get("source").unwrap();
1314-
if source.is_empty() {
1315-
error!(version_data = ?version_data, "Failed to get version data for {}", path_str);
1333+
let Ok(mod_) = mod_ else {
13161334
return None;
1317-
}
1318-
let repo = metadata.get("repo").unwrap();
1319-
let repo_name = repo.split('/').last().unwrap();
1335+
};
1336+
debug!(mod_curseforge = ?mod_);
13201337
let out = CurrentModsListItem {
1321-
name: repo_name.to_string(),
1322-
version_type: "GITHUB".to_string(),
1323-
project_id: repo_name.to_string(),
1338+
name: mod_.name,
1339+
version_type: "RELEASE".to_string(),
1340+
project_id: mod_.slug,
13241341
enabled,
13251342
};
13261343
return Some(out);

tui/src/components/home.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ impl MenuList {
7878
impl Home {
7979
pub fn new() -> Self {
8080
Home {
81-
list: MenuList::from_iter(vec![Mode::Add, Mode::QuickAdd, Mode::Toggle, Mode::List]),
81+
list: MenuList::from_iter(vec![Mode::Add, Mode::Toggle, Mode::List]),
8282
mode: Mode::Home,
8383
enabled: true,
8484
..Default::default()

0 commit comments

Comments
 (0)