Conversation
|
@Wsrsq is attempting to deploy a commit to the retrofor Team on Vercel. A member of the Team first needs to authorize it. |
Reviewer's GuideAdds a new core modpack module that can detect, parse, and import multiple modpack formats (Modrinth, CurseForge, MultiMC/PrismLauncher), including CurseForge API resolution and override file extraction, and wires it into the core module tree. Sequence diagram for modpack import with CurseForge resolutionsequenceDiagram
participant Caller
participant Modpack as ModpackModule
participant Zip as ZipArchive
participant CurseForge as CurseForgeAPI
Caller->>Modpack: import(path)
activate Modpack
Modpack->>Modpack: parse(path)
activate Zip
Modpack->>Zip: open(path)
Zip-->>Modpack: Archive
Modpack->>Modpack: parse_modrinth(archive)
Modpack-->>Modpack: Err
Modpack->>Modpack: parse_curseforge(archive)
Modpack-->>Modpack: ParsedModpack(info, files, override_prefixes)
deactivate Zip
alt modpack_type == curseforge
Modpack->>Modpack: resolve_curseforge_files(files)
Modpack->>CurseForge: cf_post /v1/mods/files(fileIds)
CurseForge-->>Modpack: file metadata (downloadUrl, modId, fileLength)
Modpack->>CurseForge: cf_post /v1/mods(modIds)
CurseForge-->>Modpack: mod metadata (classId)
Modpack-->>Modpack: build ModpackFile list with URLs and paths
end
Modpack-->>Caller: ParsedModpack(info, resolved_files, override_prefixes)
deactivate Modpack
Sequence diagram for override extraction from modpack archivesequenceDiagram
participant Caller
participant Modpack as ModpackModule
participant Zip as ZipArchive
participant FS as FileSystem
Caller->>Modpack: extract_overrides(path, game_dir, override_prefixes, on_progress)
activate Modpack
Modpack->>Zip: open(path)
Zip-->>Modpack: Archive
Modpack->>Zip: iterate entries(by_index_raw)
Modpack-->>Modpack: all_names
Modpack-->>Modpack: prefixes (existing override_prefixes)
Modpack-->>Modpack: total = count(entries matching prefixes)
loop for each archive entry
Modpack->>Zip: by_index(i)
Zip-->>Modpack: entry(name, is_dir)
Modpack-->>Modpack: relative = strip(prefix, name)
alt relative exists and within game_dir
alt entry is directory
Modpack->>FS: create_dir_all(outpath)
FS-->>Modpack: ok
else entry is file
Modpack->>FS: create_dir_all(parent)
FS-->>Modpack: ok
Modpack->>FS: File::create(outpath)
FS-->>Modpack: handle
Modpack->>FS: copy(entry, file)
FS-->>Modpack: ok
end
Modpack-->>Caller: on_progress(current, total, relative)
else
Modpack-->>Modpack: skip entry
end
end
Modpack-->>Caller: Ok(())
deactivate Modpack
Class diagram for new core modpack module types and APIclassDiagram
class ModpackInfo {
+String name
+Option~String~ minecraft_version
+Option~String~ mod_loader
+Option~String~ mod_loader_version
+String modpack_type
+Option~String~ instance_id
}
class ModpackFile {
+String url
+String path
+Option~u64~ size
+Option~String~ sha1
}
class ParsedModpack {
+ModpackInfo info
+Vec~ModpackFile~ files
+Vec~String~ override_prefixes
}
class ModpackModule {
+detect(path: &Path) Result~ModpackInfo, String~
+import(path: &Path) Result~ParsedModpack, String~
+extract_overrides(path: &Path, game_dir: &Path, override_prefixes: &[String], on_progress: Fn(usize, usize, &str)) Result~(), String~
-parse(path: &Path) Result~ParsedModpack, String~
-parse_modrinth(archive: &mut Archive) Result~ParsedModpack, String~
-parse_curseforge(archive: &mut Archive) Result~ParsedModpack, String~
-parse_multimc(archive: &mut Archive) Result~ParsedModpack, String~
-resolve_curseforge_files(files: &[ModpackFile]) Result~Vec~ModpackFile~, String~
-cf_post(client: &reqwest::Client, endpoint: &str, body: &serde_json::Value) Result~serde_json::Value, String~
-cf_class_ids(client: &reqwest::Client, mod_ids: &[u64]) HashMap~u64, u64~
-read_entry(archive: &mut Archive, name: &str) Option~String~
-read_json(archive: &mut Archive, name: &str) Result~serde_json::Value, String~
-cfg_value(content: &str, key: &str) Option~String~
-find_multimc_root(archive: &mut Archive) Option~String~
-parse_modrinth_loader(deps: &serde_json::Value) (Option~String~, Option~String~)
-parse_mmc_components(json: &serde_json::Value) (Option~String~, Option~String~, Option~String~)
}
ModpackModule ..> ModpackInfo : uses
ModpackModule ..> ModpackFile : uses
ModpackModule ..> ParsedModpack : returns
ParsedModpack o-- ModpackInfo
ParsedModpack o-- ModpackFile
File-Level Changes
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
There was a problem hiding this comment.
Hey - I've found 2 issues, and left some high level feedback:
- Using
env!("CURSEFORGE_API_KEY")hard-fails at compile time and embeds the key into the binary; consider switching to a runtime configuration mechanism (e.g., reading fromtauri.conf.json/env at startup) and passing the API key intoresolve_curseforge_filesinstead of using a global constant. - In
resolve_curseforge_files, entries whoseurldon't parse ascurseforge://projectId:fileIdare silently dropped; if mixed or malformed entries are possible it may be safer to either preserve them unchanged or surface an explicit error so they are not lost without notice.
Prompt for AI Agents
Please address the comments from this code review:
## Overall Comments
- Using `env!("CURSEFORGE_API_KEY")` hard-fails at compile time and embeds the key into the binary; consider switching to a runtime configuration mechanism (e.g., reading from `tauri.conf.json`/env at startup) and passing the API key into `resolve_curseforge_files` instead of using a global constant.
- In `resolve_curseforge_files`, entries whose `url` don't parse as `curseforge://projectId:fileId` are silently dropped; if mixed or malformed entries are possible it may be safer to either preserve them unchanged or surface an explicit error so they are not lost without notice.
## Individual Comments
### Comment 1
<location> `src-tauri/src/core/modpack.rs:351-354` </location>
<code_context>
+ id % 1000
+ )
+ });
+ let dir = match f["modId"].as_u64().and_then(|mid| class_map.get(&mid)) {
+ Some(12) => "resourcepacks",
+ Some(6552) => "shaderpacks",
+ _ => "mods",
+ };
+ Some(ModpackFile {
</code_context>
<issue_to_address>
**issue (bug_risk):** Class ID matching is currently ineffective due to pattern matching on `Option<&u64>` instead of `Option<u64>`.
`class_map.get(&mid)` returns `Option<&u64>`, so the patterns need to match on a referenced `u64`, e.g.:
```rust
let dir = match f["modId"].as_u64().and_then(|mid| class_map.get(&mid)) {
Some(&12) => "resourcepacks",
Some(&6552) => "shaderpacks",
_ => "mods",
};
```
Alternatively, bind the `&u64` to a local and match on that. This will correctly route resource and shader packs.
</issue_to_address>
### Comment 2
<location> `src-tauri/src/core/modpack.rs:310-314` </location>
<code_context>
+ .ok()
+ })
+ .collect();
+ if file_ids.is_empty() {
+ return Ok(vec![]);
+ }
+
</code_context>
<issue_to_address>
**suggestion (bug_risk):** Returning an empty list when no CurseForge IDs are parsed can silently drop files.
Since `file_ids` can be empty while `result.files` still contains valid entries (e.g. malformed or non-CurseForge ones), returning `Ok(vec![])` causes `import()` to overwrite `result.files` and drop those entries. Instead, either return the original `files` slice (e.g. via clone) when `file_ids` is empty, or short-circuit in `import()` to skip calling `resolve_curseforge_files` when there are no `curseforge://` URLs.
```suggestion
.collect();
if file_ids.is_empty() {
return Ok(result.files.clone());
}
```
</issue_to_address>Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
| let dir = match f["modId"].as_u64().and_then(|mid| class_map.get(&mid)) { | ||
| Some(12) => "resourcepacks", | ||
| Some(6552) => "shaderpacks", | ||
| _ => "mods", |
There was a problem hiding this comment.
issue (bug_risk): Class ID matching is currently ineffective due to pattern matching on Option<&u64> instead of Option<u64>.
class_map.get(&mid) returns Option<&u64>, so the patterns need to match on a referenced u64, e.g.:
let dir = match f["modId"].as_u64().and_then(|mid| class_map.get(&mid)) {
Some(&12) => "resourcepacks",
Some(&6552) => "shaderpacks",
_ => "mods",
};Alternatively, bind the &u64 to a local and match on that. This will correctly route resource and shader packs.
| .collect(); | ||
| if file_ids.is_empty() { | ||
| return Ok(vec![]); | ||
| } | ||
|
|
There was a problem hiding this comment.
suggestion (bug_risk): Returning an empty list when no CurseForge IDs are parsed can silently drop files.
Since file_ids can be empty while result.files still contains valid entries (e.g. malformed or non-CurseForge ones), returning Ok(vec![]) causes import() to overwrite result.files and drop those entries. Instead, either return the original files slice (e.g. via clone) when file_ids is empty, or short-circuit in import() to skip calling resolve_curseforge_files when there are no curseforge:// URLs.
| .collect(); | |
| if file_ids.is_empty() { | |
| return Ok(vec![]); | |
| } | |
| .collect(); | |
| if file_ids.is_empty() { | |
| return Ok(result.files.clone()); | |
| } | |
Add modpack parsing and extraction module supporting three major formats:
Details
modpack::detect()— parse a modpack zip and return metadata (name, Minecraft version, mod loader info)modpack::import()— full parse with CurseForge API resolution for download URLsmodpack::extract_overrides()— extract config/resource override files to the game directorySummary by Sourcery
Add a core modpack module to detect, import, and extract Minecraft modpacks from multiple formats.
New Features: