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.16k stars 49 forks source link

Add a variety of 'filter's for more robust mod version resolution #155

Open xane256 opened 2 years ago

xane256 commented 2 years ago

The Problem

  1. Version matching may be inaccurate for CurseForge / Modrinth mods due to mod devs not labelling version compatibility in enough detail.
  2. Version matching may be inaccurate for GitHub mods due to nonstandard naming conventions.
  3. The workaround for this is to use the flag --dont-check-game-version or the config option "check_game_version": false which introduces its own issues if either a) the profile is for a non-current game version, or b) ferium can't determine what game version the mod is for. And after updating mods or the profile game version, it can be tedious to review which mods dont need the "check_game_version": false setting anymore. The fallback feature uses the most common version patterns to help users get the mod versions they want using more robust workarounds that don't break profile configs in the future.

My Solution

Introduce a new flag --fallback=<spec> which specifies which mod version(s) to consider as compatible if the original cannot be found. This replaces --dont-check-game-version. I recommend several options for fallback specifications which collectively would fix #29, #95, and #154.

Syntax The syntax would be something like ferium add X --fallback=<spec> where <spec> is one of the following:

User Workflow and When the fallback gets used Suppose the user makes a profile using ferium add ..., then later (maybe days or weeks later) runs ferium upgrade. When creating a profile the user may have to specify fallback options to get compatible versions of mods, or simply accept that some mods may not be compatible (#142). When they run ferium upgrade, an updated mod version which exactly matches the profile game version may be available and should be used instead. Second, the actual mod version that gets used as the result of a fallback option may be updated, so that's important to detect when running ferium upgrade. I think this logic fits in with what ferium does already but I wanted to be specific.

Max Version Constraints If this gets implemented I would also propose separate and independant flag to constrain the mod versions that are considered compatible. The purpose would be to future-proof the use of fallback=latest. Another way to do that would be exporting exact mod versions. Perhaps something like one of these:

JustSimplyKyle commented 2 years ago

ferium add X --fallback=<spec> Spec "organize"(I remove the reasoning, only left what I think what the spec means)

Personal opinion: I personally think https://github.com/gorilla-devs/ferium/issues/29 should be still implemented, because there are still github mod that includes -sources which can not be excluded out by only using <spec>

xane256 commented 2 years ago

I spent some time thinking about the logic for how to change libium's upgrade code (the "check" function) to improve the built-in checking, add fallback support and even a way to plug-in the custom filtering for #29. But I'm interested to hear from @theRookieCoder if they have ideas for that already

theRookieCoder commented 2 years ago

I think this is great solution actually! Currently my idea is to create multiple lists of the mods (maybe using a hashmap to reduce duplications) and filter them. Then we can intersect the lists to get the compatible mods, and of course pick the latest version. This would also allow for more constructive feedback on why a mod was unable to be resolved (#76)

xane256 commented 2 years ago

@theRookieCoder I came up with this idea. (Also side note I sent a discussion the other day was curious if you saw)

I know this post is long but I think the logic is good so I ended up making it more explicit. I think a lot of this will quickly make sense to you and I'm also happy to hear your feedback on this or more on the hash table idea.

Filter System

Hierarchy: The github release / asset structure is hierarchical or like a tree with depth 2. The first level is the release, the second level is the asset within the release. Filters in this context can apply to either level, and in other contexts / problems this kind of thing might be useful for even deeper structures.

Filter: A filter takes a set of objects (either releases in a repo, or assets within a release) and strategically prunes the set using tags, or pieces of info / metadata extracted (via regex) from the objects. A filter f updates a set S by S = f(T_good, T_bad, S) where T_good is a set of "good" tags and T_bad is a set of "bad" tags. The good / bad tags can be explicit, or the filter may just have logic for checking if a given tag is a good or bad one (see version matching below). For example, in a filter for checking mod loaders for a fabric profile, fabric is a good tag and forge is a bad tag. For each element x of the set, the filter extracts tokens from x and tracks whether they are "good" or "bad". To use the results of filters, we remove / keep elements of the set based on which good/bad tags they have, and whether other elements also have those tags.

There are two types of filters we'll use:

New Compatibility Checking

[footnote 1] - I know libium just searches these in order, but for this set-based logic we can reduce this set by:

[footnote 2] - A previous regex I tried missed the 1-16-3 in the string file_v2_1-16-3_2_fabirc-foo.jar. One way around this would be to find the first match in the string, (the 2_1), extract it, then remove characters from the front before the match, plus one extra, so you restart with _1-16-3_2_fabirc-foo.jar and find the next match. That would definitely get all possible offsets of the pattern. Then check every token using the version pattern matching to decide which tokens are "good" or "bad". If the regex is more general, it's more likely to match part of other numbers in the string. If that happens, a weak filter is still a great idea. But with a very strict regex we can apply a strong filter right away.

Regexes

Basic regexes:

# You can try these on https://regexr.com or https://regex101.com
# version number - has numbers separated by '.'
([0-9]+)(\.([0-9]+))*

# Minecraft version numbers (V1): Extracts major version and minor version, also detects snapshots.
# Its possible this could pull out version numbers for things that are not minecraft.
# Note the period separator might be `.` or `-` or `_` in practice, but it probably 
(?<![0-9])(([1-9])\.([0-9]+))(\.(0|([1-9]+))){0,1}(\+{0,1})|([0-9]{2}[a-z][0-9]{2}[a-z])

Minecraft Version Regex (V2): Way better.

magneticflux- commented 2 years ago

I'd like to contribute a basic implementation that I believe covers 80% of the use cases described: Whenever No compatible file was found would be returned during ferium upgrade, it would instead warn in yellow and pick the most recent version compatible with a Minecraft version <= the selected one.

This could be done relatively easily and allows the workflow of "set new Minecraft version, try to upgrade everything at once, test if mods are still compatible, upgrade to guaranteed compatible versions eventually" that I personally use for minor releases.

Regarding the complete implementation, I believe you would have to have some sort of constraint solver to resolve valid version sets reliably. The good_lp crate is one option.

magneticflux- commented 2 years ago

Here's a quick-and-dirty proof-of-concept to get my modpack updated:

Index: src/upgrade/check.rs
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
diff --git a/src/upgrade/check.rs b/src/upgrade/check.rs
--- a/src/upgrade/check.rs  (revision 337cfe81412aa180f2646f26623c9285a17dc914)
+++ b/src/upgrade/check.rs  (date 1658983776949)
@@ -1,13 +1,25 @@
-use crate::{config::structs::ModLoader, version_ext::VersionExt};
 use ferinth::structures::version_structs::{Version, VersionFile};
 use furse::structures::file_structs::File;
+use lazy_regex::regex_find;
 use octocrab::models::repos::{Asset, Release};

+use crate::{config::structs::ModLoader, version_ext::VersionExt};
+
+fn strip_minor_mc_version(version: &str) -> String {
+    let result = regex_find!(r#"\d+\.\d+"#x, version);
+    return result.unwrap_or(version).to_string();
+}
+
 /// Check if the target `to_check` version is present in `game_versions`.
 fn check_game_version(game_versions: &[String], to_check: &str) -> bool {
     game_versions.iter().any(|version| version == to_check)
 }

+/// Check if the target `to_check` version or a slightly older version is present in `game_versions`.
+fn check_game_version_relaxed(game_versions: &[String], to_check: &str) -> bool {
+    game_versions.iter().any(|version| version == to_check || version == &strip_minor_mc_version(to_check))
+}
+
 /// Check if the target `to_check` mod loader is present in `mod_loaders`
 fn check_mod_loader(mod_loaders: &[String], to_check: &ModLoader) -> bool {
     mod_loaders
@@ -27,11 +39,21 @@
     files.sort_unstable_by_key(|file| file.file_date);
     files.reverse();

-    for file in files {
+    for file in files.iter() {
         if (Some(false) == should_check_game_version
             || check_game_version(&file.game_versions, game_version_to_check))
             && (Some(false) == should_check_mod_loader
                 || check_mod_loader(&file.game_versions, mod_loader_to_check))
+        {
+            return Some(file);
+        }
+    }
+    // No exact match found, relax to allow slightly older versions
+    for file in files.iter() {
+        if (Some(false) == should_check_game_version
+            || check_game_version_relaxed(&file.game_versions, game_version_to_check))
+            && (Some(false) == should_check_mod_loader
+            || check_mod_loader(&file.game_versions, mod_loader_to_check))
         {
             return Some(file);
         }
@@ -47,11 +69,21 @@
     should_check_game_version: Option<bool>,
     should_check_mod_loader: Option<bool>,
 ) -> Option<(&'a VersionFile, &'a Version)> {
-    for version in versions {
+    for version in versions.iter() {
         if (Some(false) == should_check_game_version
             || check_game_version(&version.game_versions, game_version_to_check))
             && (Some(false) == should_check_mod_loader
                 || check_mod_loader(&version.loaders, mod_loader_to_check))
+        {
+            return Some((version.get_version_file(), version));
+        }
+    }
+    // No exact match found, relax to allow slightly older versions
+    for version in versions.iter() {
+        if (Some(false) == should_check_game_version
+            || check_game_version_relaxed(&version.game_versions, game_version_to_check))
+            && (Some(false) == should_check_mod_loader
+            || check_mod_loader(&version.loaders, mod_loader_to_check))
         {
             return Some((version.get_version_file(), version));
         }
Index: Cargo.toml
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
diff --git a/Cargo.toml b/Cargo.toml
--- a/Cargo.toml    (revision 337cfe81412aa180f2646f26623c9285a17dc914)
+++ b/Cargo.toml    (date 1658982430736)
@@ -34,6 +34,7 @@
 ] }
 tokio = { version = "~1.20.0", default-features = false, features = ["fs"] }
 rfd = { version = "~0.9.1", default-features = false, optional = true }
+lazy-regex = "~2.3.0"
 serde = { version = "~1.0.139", features = ["derive"] }
 clap = { version = "~3.2.12", features = ["derive"] }
 url = { version = "~2.2.2", features = ["serde"] }
jhmaster2000 commented 2 years ago

https://github.com/gorilla-devs/ferium/issues/155#issuecomment-1171921778

Please add onto this resolution logic the scanning of release names for game versions and mod loaders, not only assets, the current scanning of only assets prevents Earthcomputer/clientcommands from working without both --dont-check-* flags despite having perfectly sane and clearly labelled releases, except it's on the release name instead of the jar file.

Sure it doesn't have any mod loader indicator but it only supports Fabric, so using --dont-check-mod-loader is fine here.

theRookieCoder commented 1 year ago

I've decided to call these filters. Basically, they will run on the list of versions that a project has. For example if there is a mod loader filter, you can set it to filter out only Fabric mods, or allow both Fabric and Quilt mods (which will be the default when running Quilt). Then another filter, like a game version filter, will run again on the list of mods to determine versions compatible with the game version configured. The release channel can also be a filter. These will then be intersected to form a final list of versions, from which the latest one will be picked automatically or this list will be shown to the user (#95). The intersection feature will hopefully resolve #76.

Hopefully being able to manually pick exactly what to filter will make ferium's version resolution very powerful.

theRookieCoder commented 1 year ago

Here is a list of filters I plan on implementing. Please do provide feedback.

These will all contain lists of values, so they will be highly configurable. There will also be 'negative' variants to exclude patterns, specifically for the GitHub ones.

theRookieCoder commented 1 year ago

The custom URL download will be part of #141