Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
xane256 opened this issue Jun 28, 2022 · 10 comments
Open

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

xane256 opened this issue Jun 28, 2022 · 10 comments
Labels
enhancement Improvements to the project

Comments

@xane256
Copy link

xane256 commented Jun 28, 2022

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:

  • latest: use the latest version of the mod available. This is the easiest way to get a mod to work when the profile version is the latest minecraft version. However it has a significant flaw - it is not future-proof. Creating a profile today with this setting may work now but might break when mods update. The current game version check shares this issue, and as a result I would like to propose a way to save / export a profile that saves a specific jar file URL with each mod that points to the exact URL that ferium upgrade would pull from. That way, when a user gets a profile working (after troubleshooting launching & ingame mod conflicts) they can go back to ferium and "lock" the versions to make the profile to work at any point in the far future (see url option below).
  • minor: The latest minor version for the same major version. If the profile is set to 1.18 or 1.18.3 for example, then setting --fallback=minor means the latest mod release (maybe its earlier, like 1.18.1, or later, like 1.18.9) should be considered compatible. This option is almost the same as latest except it would not download a 1.19 version into a 1.18.2 profile. Think of this as the "probably future-proof version of latest." The "probably" comes from the fact that I don't know what to do if the game version can't be decided, OR if the mod later releases a breaking update for a later minor version.
  • previous: Attempt to use the mod version for the minor game version preceeding the profile game version. Example: If 1.18.2 does not exist, previous is short for "1.18.1". If the profile is set to 1.18.1, then "previous" means 1.18. This would be the ideal setting for making a 1.16.5 profile where almost all 1.16.4 mods would work just fine because 1.16.5 had only a couple small security changes. More generally this option is to address mods which don't update compatibility info for a minor version update because the previous version still works.
  • major: Consider any mod release for the previous major version to be compatible. This is the perfect solution for mods like FakeDomi/FastChest which "bucket" their releases by major version.
  • x.y.z (number / version string): A comma-separated list of version numbers to check in order if the exact version fails. For example ferium add X --fallback=1.18.1,1.18 would attempt to find a mod release for 1.18.1 first, then try 1.18 after.
  • url: a URL to a specific github release, github jar file within a release, curseforge file download url etc. Github jar file URLs at a minimum, compatibility with the "save" feature idea above at a maximum. Tells ferium explicitly where to find a mod if it doesn't find a matching version. This is necessary for mods such as A5b84/dark-loading-screen which only lists version compatibility in their changelog.

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:

  • --max-version=profile: If a mod which is marked compatible via a fallback has a version which ferium can detect and the version is greater than the profile game version, this option marks the candidate as incompatible.
  • --max-version=major: If the profile is for 1.18.1 or 1.18, this disqualifies mods which have an identifiable version of 1.19.x / 1.19+.
  • --max-version=<date>, where <date> is something like 20220623 which restricts compatibility to releases before this date
@xane256 xane256 added the enhancement Improvements to the project label Jun 28, 2022
@xane256 xane256 changed the title Fallback option to resolve mod version incompatibility Add Fallback option to resolve mod version incompatibility Jun 28, 2022
@JustSimplyKyle
Copy link
Contributor

JustSimplyKyle commented Jun 29, 2022

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

  • latest -- Download whatever is the newest. ( Currently what --dont-check-game-version do)
  • minor -- Any minor version for the same major version.
  • previous -- One minor version before the profile game version.
  • major -- Consider any mod release for the previous major version to be compatible.
  • x.y.z -- Specific mod version, can select multiple by using a comma.
  • url -- Download the mod at the url.

Personal opinion:
I personally think #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
Copy link
Author

xane256 commented Jun 30, 2022

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
Copy link
Collaborator

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
Copy link
Author

xane256 commented Jul 1, 2022

@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:

  • Strict / Strong Filter: The set S is reduced to include only the elements for that contain no bad tags. So if something has no parseable tags, it stays, and if it has some tags which are all good, it also stays. It's important for these filters to have high confidence when removing objects. This is to prevent desired objects from being removed when they are unusual.

  • Weak Filter: The filter keeps elements which have good tags, and removes elements which have bad tags, but also accounts for whether other elements have good/bad tags.

    • For each object S[i] extract the tags to a set T[i]. For example, you can use a regex to extract version numbers (See actual regex below) and each one could be a separate tag.
    • Set T_common to be the intersection of all T[i], i.e. the tags common to all elements. Splitting a filename up by word boundaries or - or _ characters will (usually) make the common tags include things like the mod's own version or build number.
    • Set U[i] to be the tags in T[i] that are not in T_common - these are the "unique" tags for each element. These tags are most likely to have identifying numbers & tokens that make the difference between which objects to keep and which to throw away.
    • If any U[i] contains a "good" tag, discard all objects j where U[j] has at least one bad tag and U[j] has no good tags.
      • Here's the reasoning. If any U[i] has a "good" tag, it means this filter / test has some usable power to distinguish "good" objects from "bad" ones. But it might not be 100% determinative, so we'll only use it to prune the objects we can tell are "bad", and we keep all the "mixed good + bad" objects, to hopefully sort later with other tests.
      • Example: A release name could conceivably say Release 1.2.0 for Forge 1.16 and Fabric 1.17+. A filter to check mod loaders should keep it because it has fabric in the name, not discard it based on having forge. And a filter for version matching should keep it because even though it has 1.2.0 and 1.16 in the name, (both bad tags), it also has 1.18+ which is good. The release is kept so we can search the assets later and find the fabric release among them using another filter later. If other releases have forge only, they are removed because at least one says fabric. If the latest release for this mod doesn't have any loader in the release name, its kept because it doesn't have any bad tags for the loader filter.

New Compatibility Checking

  • Get all the latest GH releases [footnote 1].
  • First do a modified version of the existing "best-case" check that libium already does, but also cache some info to use later if it fails.
    • for each asset of each release, ordered newest to oldest:
      • discard if its *.zip or *sources* or *dev* or not *.jar
      • ML check: If the repo name or release name or asset name contains the mod loader name: pass
      • Version: If the profile (game) version appears in the asset name: pass
      • if the file passes both tests, use it. Otherwise add the release & asset to a set of candidates for filtering
  • Copy the set of releases / asset objects in case we resort to a fallback option (Add a variety of 'filter's for more robust mod version resolution #155). To use a fallback option we come back to this point but use different logic for deciding which tags are good or bad.
  • Apply the following filters at the release level:
    • Weak filter for the version. Good tags include the exact version, or tokens like 1.13+. For fallback=major, with a 1.18 profile, 1.18 is a good tag. Tags should be extracted with regex, or since we're parsing release names, just separate the name at whitespace. Not every token or word is either good or bad, the point is to find whatever bad ones or good ones are there.
    • Weak filter for the mod loader. Use fabric, forge, quilt as tags, extract words with regex to check which tags are there.
    • Weak filter for operating system. For mac / unix / linux, good = macos / unix / linux, bad = windows. For linux, macos and windows are bad, for windows mac / unix / linux are bad.
  • With the new refined set of releases:
    • Do not aggregate all assets into one big set. Check each release individually and reduce the candidate assets within each one to find the best asset for each release. Save those in a set A and return the newest one at the end.
    • If there's only one asset in the release, add it to A.
    • Otherwise filter for version. See [footnote 2]. I think this dynamic approach should be pretty good:
      • Extract version tokens (substrings) from the asset name. "Good" tags are ones that match the profile version using the version pattern matching below. Bad ones are things that dont match.
      • If any U[i] has at least 2 unique tags, the version extraction can't decide what the real version is. In this case reduce assets using a weak filter.
        • Otherwise every U[i] has size 0 or 1 - in this case add each asset to A as long as it has no "bad" tags (aka just use a strong filter).
    • Past this point the logic gets pretty difficult because I think these cases are very rare. I think if there are any assets left with at least one good tag, we can add them to A.

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

  • only check 10 releases initially, then restart with more if we're not confident in the asset we get
  • only consider releases posted after the launch date of the mc version for the profile (idk I think some mods still release "early", this definitely the hardest option)

[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.

  • The pattern (\.(x|0|([1-9]+))){0,1} is for the end of the version. It matches .x, .0, .2 etc, or nothing, as in 1.18.
  • The pattern (\+{0,1}) captures a + at the end if there is one
  • The pattern assumes the version starts with 1.## but its more generous if there is an mc in front
  • The pattern (?<![0-9]) says there can't be a digit right before the 1 at the beginning
  • The bit at the end is for snapshots, like 21w34a.
(mc)(.?)[1-9].([0-9]+).(x|0|([1-9]+)){0,1}(\+{0,1})|
(?<![0-9])(
((1)\.([0-9]{2}))(\.(x|0|([1-9]+))){0,1}(\+{0,1})|
((1)\-([0-9]{2}))(\-(x|0|([1-9]+))){0,1}(\+{0,1})|
((1)\_([0-9]{2}))(\_(x|0|([1-9]+))){0,1}(\+{0,1}))|([0-9]{2}[a-z][0-9]{2}[a-z])
  • Version Pattern Matching How to tell if a version token (extracted by regex) matches a game version:
    • Here, X, Y, Z are all
    • a version range X-Z matches game version Y if (X,Y,Z) is sorted, example 1.15-1.18.3 matches 1.15
    • a version range X+ matches game version Y if (X,Y) is sorted

@theRookieCoder theRookieCoder changed the title Add Fallback option to resolve mod version incompatibility Add fallback options to resolve mod version incompatibility Jul 17, 2022
@theRookieCoder theRookieCoder changed the title Add fallback options to resolve mod version incompatibility Add fallback options for more robust mod version resolution Jul 17, 2022
@magneticflux-
Copy link

magneticflux- commented Jul 28, 2022

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-
Copy link

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
Copy link

#155 (comment)

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 theRookieCoder changed the title Add fallback options for more robust mod version resolution Add a variety of 'filter's for more robust mod version resolution Mar 23, 2023
@theRookieCoder
Copy link
Collaborator

theRookieCoder commented Mar 23, 2023

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
Copy link
Collaborator

theRookieCoder commented Mar 23, 2023

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

  • Mod loader
  • Game version
  • Game version (minor)
    This will also allow any game versions that were considered to not have significant changes between them, e.g. 1.19 and 1.19.1.
    This will be determined using Modrinth's version list tag
  • Release channel
    Alpha and Beta will be mapped to prerelease in GitHub releases. A separate release channel filter for GitHub releases will also be available.
  • Compatibility range
    This filter could have custom mechanisms so that it is easy to switch between client and server side downloads, or even download both simultaneously to different directories.
  • GitHub regex/glob for the
    • Release name
    • Release description
    • Asset name (filename)

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
Copy link
Collaborator

The custom URL download will be part of #141

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement Improvements to the project
Development

No branches or pull requests

5 participants