Open xane256 opened 2 years ago
ferium add X --fallback=<spec>
Spec "organize"(I remove the reasoning, only left what I think what the spec means)
--dont-check-game-version
do)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>
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
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)
@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.
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.
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.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.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.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.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.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.*.zip
or *sources*
or *dev*
or not *.jar
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.fabric
, forge
, quilt
as tags, extract words with regex to check which tags are there.macos
/ unix
/ linux
, bad = windows
. For linux, macos
and windows
are bad, for windows mac
/ unix
/ linux
are bad.A
and return the newest one at the end.A
.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.
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).A
.[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.
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:
X, Y, Z
are all X-Z
matches game version Y
if (X,Y,Z)
is sorted, example 1.15-1.18.3
matches 1.15
X+
matches game version Y
if (X,Y)
is sortedI'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.
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"] }
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.
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.
Here is a list of filters I plan on implementing. Please do provide feedback.
1.19
and 1.19.1
.
This will be determined using Modrinth's version list tagThese 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.
The custom URL download will be part of #141
The Problem
--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 thatferium 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 (seeurl
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 aslatest
except it would not download a 1.19 version into a 1.18.2 profile. Think of this as the "probably future-proof version oflatest
." 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 likeFakeDomi/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 exampleferium 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 asA5b84/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) runsferium 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 runferium 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 runningferium 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 for1.18.1
or1.18
, this disqualifies mods which have an identifiable version of1.19.x / 1.19+
.--max-version=<date>
, where<date>
is something like20220623
which restricts compatibility to releases before this date