gorilla-devs / ferium

Fast and multi-source CLI program for managing Minecraft mods and modpacks from Modrinth, CurseForge, and GitHub Releases
Mozilla Public License 2.0
1.13k stars 47 forks source link

Add more contextual informations to actions #242

Closed T3sT3ro closed 1 year ago

T3sT3ro commented 1 year ago

The problem

There are some actions that produce output on terminal, which would be a lot more helpful if it included some additional info.

My solutions

Moved to #250 - When `ferium add` results in "project/repository is not compatible": - print WHY: minecraft version? Mod loader? License or 3rd party tool access disabled by moderators? - Link to info about overrides as part of the error message - `ferium profile` should print current profile first, then a simple text saying "Use `ferium profile -h` for more info about this command" - `ferium list` should print first a header with profile name, loader and MC version and only then list of mods. - `ferium modpack` should show current modpack, then a simple prompt with "Use `ferium modpack -h` for more info about this command" - `ferium profile switch` should print in a row profile name, mc version, loader and number of mods - `ferium profiles` should be an alias to `ferium profile list` - `ferium modpacks` should be an alias to `ferium modpack list` - `ferium mods` should be an alias to `ferium list` (because the `list` is the only unscoped subcommand which doesn't align with `profiles list` and `modpacks list`). - `ferium info` should be added and show currently set profile and modpack - `ferium remove` should print `removed `
theRookieCoder commented 1 year ago

print WHY: minecraft version? Mod loader? License or 3rd party tool access disabled by moderators?

76

The mod list should also include a version of the mod

Does this mean the current version installed, or the latest compatible version? Unfortunately both are not feasible, I've discussed the former in #138 and the latter is just way too slow.

These are great suggestions! I will have to think about some of these, and some features are just not worth the effort (e.g. dependency listing). They are waaay more complicated to implement than it might seem, and it might not be worth it for such a small improvement.

T3sT3ro commented 1 year ago

Does this mean the current version installed, or the latest compatible version?

I was thinking about currently installed version, which is almost always present in the filename. Currently the output is of the form:

$ ferium list
Cloth Config API (Fabric/Forge)               CurseForge 348521
Fabric API                                    Modrinth   P7dR8mSH

but the ferium upgrade update already contains this info in the form:

$ ferium upgrade
...
✓ Cloth Config API (Fabric/Forge)             cloth-config-8.2.88-fabric.jar
✓ Fabric API                                  fabric-api-0.68.0+1.19.2.jar
...

So I think it would be beneficial to print just that - the filename of the latest mod, which helps in identifying the version easily:

Cloth Config API (Fabric/Forge)     CurseForge 348521   cloth-config-8.2.88-fabric.jar
Fabric API                          Modrinth   P7dR8mSH fabric-api-0.68.0+1.19.2.jar

This is the most straightforward way, in my opinion, but additional work could be done to provide a RegExp for extracting the version from the mod name using some most common patterns. This would on the other hand require you to sometimes update the regex if some new format is reported. Some example formats from the top of my head could fit the following regex: [^\d]*(\d+(\.\d+(.\d+(.\d+([^\d]?.*)?)?)?)?)

Which should match any of the following:

theRookieCoder commented 1 year ago

What you're describing is actually a huge problem that this program has had to deal with since the very beginning. (see #17, yes that old) The main issue is that using the filename to determine the mod version is unreliable because naming conventions differ quite a bit between authors, mod loaders, etc.

T3sT3ro commented 1 year ago

Note, that my main proposal is to simply display the filename, and depend on human eyes to read the version number.

Using the filename to determine the mod version, on the other hand, would be problematic, and it probably would be a dirty solution with a long cycle of proposing another pattern matcher and adapting it, while also never being sure, that ALL versioning schemes used in the wild are handled.

While thinking about that I came up with yet another solution. Mods written for forge, fabric or quilt all have some kind of file, which has some mod metadata inside of them:

Commands I use to extract version information:

This approach would be the most correct one, as it depends on a common, standardized by mod-loaders mod structure. Versions would have to be extracted using appropriate json and toml parsers for fabric/quilt and forge respectively.

It would be that simple, because jar files are just zip files with different extension name, so any tool and library that can peek inside the zip file could read mod's version. What's more, this approach could be useful to parse additional mod information that is included in the mod's config files, such as description, author, dependencies (as listed in fabric.mod.json, author, URL etc.). What's great about that is that the layout of configuration file is specified in the corresponding forge/fabric/quilt documentation, so there is no quessing, and if the mod does not adhere to standard, then bugs would be reported to corresponding mod developers to fix them.

theRookieCoder commented 1 year ago

Yes that would be a solution, I think PrismMC uses this. I was thinking that getting the files, unzipping them, and reading the manifests would be too slow, but I guess I should try it out one day.

T3sT3ro commented 1 year ago

I had to check to be sure, but according to wikipedia the zip file doesn't need to be decompressed as a whole to access separate file. Zip file is capable of storing multiple files each compressed using different method, as well as decompressing a part of the whole archive — in this case, the fabric.mod.json/quilt.mod.json/META-INF/mods.toml/META-INF/MANIFEST.mf. The key would be to find a library (or to write a utility function yourself) that extracts required files. This method would have to read the list of the files in zip archive (called central directory and stored at the end of the file, as described on wiki), offset into the file and decompress a single file.

There is a zip crate probably capable of doing that.

T3sT3ro commented 1 year ago

Here is a concept that solves just that, written with my very basic knowledge of Rust. First, dependencies:

# Cargo.toml
[dependencies]
zip = "0.6"
serde = "1.0"
serde_json = "1.0"
toml = "0.5"
regex = "1.7"

and here is the script:

use serde_json::{Result as JsonRes, Value as JsonVal};
use std::{fs::File, io::Read};
use toml::Value as TomlVal;
use zip::{read::ZipFile, ZipArchive};
use regex::Regex;

fn main() {
    let jar_path = std::env::args().nth(1).expect("pass *.jar file path");
    let t = get_version(jar_path);
    println!("{}", t);
}

fn get_version(jar_path: String) -> String {
    let jar_file = File::open(jar_path).expect("File not found");
    let mut jar_archive = ZipArchive::new(jar_file).expect("Can't open .jar file");

    match jar_archive
        .by_name("fabric.mod.json")
        .map(get_version_string_from_json)
    {
        Ok(ver) => return ver,
        Err(e) => e,
    };

    match jar_archive
        .by_name("quilt.mod.json")
        .map(get_version_string_from_json)
    {
        Ok(ver) => return ver,
        Err(e) => e,
    };

    match jar_archive
        .by_name("META-INF/mods.toml")
        .map(get_version_string_from_toml)
    {
        Ok(ver) => {
            return if ver != "${file.jarVersion}" {
                ver
            } else {
                jar_archive
                    .by_name("META-INF/MANIFEST.MF")
                    .map(get_version_string_from_manifest)
                    .expect("cannot determine version")
            }
        }
        Err(_) => panic!("cannot determine version"),
    };
}

fn read_zip_file_content(mut zip_file: ZipFile<'_>) -> String {
    let mut content = String::new();
    let _n = zip_file
        .read_to_string(&mut content)
        .expect("couldn't read file");
    return content;
}

fn get_version_string_from_json(zip_file: ZipFile<'_>) -> String {
    let content = read_zip_file_content(zip_file);
    let config: JsonRes<JsonVal> = serde_json::from_str(content.as_str());

    return String::from(
        config.expect("couldn't read json")["version"]
            .as_str()
            .expect("missing version string in json"),
    );
}

fn get_version_string_from_toml(zip_file: ZipFile<'_>) -> String {
    let content = read_zip_file_content(zip_file);
    let config: TomlVal = toml::from_str(content.as_str()).expect("can't read toml");
    // println!("{:?}", config["mods"][0]["version"].as_str());
    return String::from(config["mods"][0]["version"].as_str().expect("missing version string in toml"));
}

fn get_version_string_from_manifest(zip_file: ZipFile<'_>) -> String {
    let content = read_zip_file_content(zip_file);
    let re  = Regex::new(r"Implementation-Version: (?P<version>.*)").unwrap();
    let caps = re.captures(content.as_str());
    return String::from(&caps.unwrap()["version"]);
}

It requires a thorough check, of course, but it is capable of extracting version info from fabric, quilt(probably, untested) and forge mods when given mod *.jar file. It doesn't decompress whole file, just extracts a single file from the zip.

The important part is using ZipArchive::new(jar_file).by_name("some.jar").read_to_string(s)

I wanted it to be more concise, but I for the love of god don't know how to chain results and deal with lifetime problems of ZipArchive.

theRookieCoder commented 1 year ago

Oh yeah I do remember now, it would be possible to access only the manifest. In fact I do already use the zip crate in libium to unzip modpacks.

theRookieCoder commented 1 year ago

Alright well there is still the issue of linking the mod file and the mod entry within ferium, how can that be done? This is the reason I had refused to implement #138

theRookieCoder commented 1 year ago

I've moved your recommendations about the CLI to #250. I've updated your initial comment to collapse those and link to the new issue. The 2 points we discussed here are left outside, which are unfortunately not feasible at the moment.

T3sT3ro commented 1 year ago

Alright well there is still the issue of linking the mod file and the mod entry within ferium, how can that be done? This is the reason I had refused to implement #138

I would need some more context. For now, the most straightforward I can imagine is storing the path to the file jar along with the SHA256 hash of a mod jar in the profile for the project ID. This way you can have both quick access to jar file, as well as validation that the file you point to is the actual file in question. If the file doesn't exist OR the SHA256 of jar is invalid, appropriate message should be displayed.