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

Rework mod discovery, resolution and loading #461

Merged
merged 5 commits into from
Jul 3, 2021

Conversation

sfPlayer1
Copy link
Contributor

@sfPlayer1 sfPlayer1 commented Jun 29, 2021

changes

  • the input mod candidates are now allowed to have duplicates, Loader will select the most recent compatible version among the mods directory contents, any nested mods and other sources
  • nested mods won't be loaded unless the encompassing mod is also loaded
  • nested mods no longer get added before they are determined to be active
  • mod resolution errors are somewhat improved, "empty clause" and similar should be gone
  • mod discovery handles nested mods more efficiently
  • metadata parsing rewinds the gson reader instead of re-reading the raw file
  • mod resolution supports load conditions now
  • mod resolution biases towards including recommended mods
  • mod resolution determines the immediate failure cause, the minimal set of broken dependencies overall and the best solution to fix the problem (which mods to add/remove/replace)
  • mod selection priorities take the nesting level into account if the versions are identical
  • nested mods use regular files in .fabric/processedMods instead of jimfs
  • mod resolution is now compatible with vanilla sat4j
  • the mod dependency api carries the dependency kind instead of 5 kind specific getters
  • removed practically unused and subpar n.f.loader.api.VersionPredicate
  • added new predicate api, including parsing, evaluation and inspection
  • migration from URL to Path for mod origins
  • moved metadata checking to metadata parsing
  • mod load order changed due to implementation changes, but this is undefined territory anyway
  • the mod order will be randomized in-dev
  • new system property -Dfabric.debug.loadLate=modA,modB,.. to move mods to the end of the load order for debugging/workaround purposes
  • new system property -Dfabric.debug.throwDirectly to disable exception gathering while invoking entry points etc and throw the originally encountered exception directly (helps with misleading console output while debugging)

associated future changes

  • make use of the extra resolution failure data
  • better mod resolution errors and display in general
  • expose mod load conditions (api+json)
  • negative bias from conflicts declarations
  • sat4j shading

Comment on lines 88 to 101
@Override
public String toString() {
if (min == null) {
if (max == null) {
return "(-∞,∞)";
} else {
return String.format("(-∞,%s%c", max, maxInclusive ? ']' : ')');
}
} else if (max == null) {
return String.format("%c%s,∞)", minInclusive ? '[' : '(', min);
} else {
return String.format("%c%s,%s%c", minInclusive ? '[' : '(', min, max, maxInclusive ? ']' : ')');
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think it's a good idea to use mathematical notation for a toString implementation. I'd avoid using toString for user-readable information, and even then, this notation in general is not user-friendly.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This isn't supposed to be interpreted by the user either, IMO it does a fair job for its main applications (devs working on version analysis similar to my installable mod inference code).

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would say +inf is better than +∞, just in case some people's computer is not UTF-8 encoding.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Java should convert it properly, I'd rather have a more compact format for this niche thing than not.

{
"name": "com.google.jimfs:jimfs:1.2-fabric",
"url": "https://maven.fabricmc.net/"
},
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does the new ow2 sat4j need to be shipped though?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The plan is to shade it, so not relevant for the installer.

@liach
Copy link
Contributor

liach commented Jun 29, 2021

These changes are too big. I think the better way to review is to just look at the source code without looking at the diff.

@sfPlayer1
Copy link
Contributor Author

These changes are too big. I think the better way to review is to just look at the source code without looking at the diff.

Most of the n.f.loader.discovery classes are practically rewritten, checking the diff for them is indeed rather pointless

Copy link
Member

@modmuss50 modmuss50 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good, too much to do an indpeth review but looks a lot more approachable overall.


/**
* Represents a version of a mod.
*
* @see ModMetadata#getVersion()
*/
public interface Version {
public interface Version extends Comparable<Version> {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there any concern here from potential existing implementations which wouldn't implement Comparable? I can't think of any but it is technically a backward compatibility problem

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, are users even supposed to implement this interface?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

They shouldn't need to, but that hasn't stopped people doing odd things with internals in the past

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This isn't really supported and somewhat pointless (can't put that Version anywhere in loader). The JVM doesn't verify whether a class implements everything, so would only throw AbstractMethodError on access.

solverPrepTime = System.nanoTime();

IPBSolver solver = SolverFactory.newDefaultOptimizer();
solver.setTimeout(60); // 60s
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should it ever be taking close to a minute to resolve mod dependencies?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should be <1s even for a large modpack, but doesn't hurt to leave some room. I went for 60s because it's the most I'd want to wait. It actually usually recovers itself when the limit is hit, just proceeding with a potentially non-optimal result.

int[] comps = ((SemanticVersionImpl) v).getVersionComponents();

if (pr == null) { // no prerelease, use large pr segment
pr = "zzzzzzzz";
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would this be fine if someone used a prerelease of zzzzzzzzz (ie with one more z)?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If said mod is present, the condition a line above fails and selects another code path, otherwise the least mod version suggested to be installed will be slightly off.

}

private static String getCandidateFriendlyVersion(ModCandidate candidate) {
return candidate.getVersion().getFriendlyString();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will (the use of) this result in errors like The developer(s) of x_mod have found that version * of y_mod critically conflicts with their mod.? If so, where the version is wildcarded could it be made clearer along the lines of The developer(s) of x_mod have found that y_mod critically conflicts with their mod., so that it is more apparent there is no version which could ever work?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think so, ModCandidate is always a concrete mod, not a dependency reference.

Collection<ModDependency> getDepends();
@Deprecated
default Collection<ModDependency> getDepends() {
return getDependencies().stream().filter(d -> d.getKind() == ModDependency.Kind.DEPENDS).collect(Collectors.toList());
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are five methods with the same implementation, except for their kind filter. I would create an abstract method that uses a parameter to dictate the kind filter instead.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Java 8 doesn't allow private interface methods, so can't factor it out without accessing the implementation or adding even more methods. These API methods are practically unused, so no need to add convenience helpers..

Set<VersionPredicate> getVersionRequirements();
Collection<VersionPredicate> getVersionRequirements();

enum Kind {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not a find of 'kind'. Would 'type', 'relation', or 'relationship' be better here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"type" is too overloaded with other meanings in this concept, relation* is wrong since ModDependency itself is the relation. Not sure what's wrong with calling depends, recommends , ... kinds of dependencies.

Kind(String key, boolean positive, boolean soft) {
this.key = key;
this.positive = positive;
this.soft = soft;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd document this better and give it a more meaningful name to clarify that dependency kinds that are soft only produce warnings rather than failing mod resolution.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it's to indicate it being a soft dependency (opposite of a hard dependency), whether it produces a warning is unspecified (suggests doesn't, recommends/conflicts do)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is a hard dependency?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hard = must be met, soft = should be met

import net.fabricmc.loader.api.Version;
import net.fabricmc.loader.impl.util.version.SemanticVersionImpl;

public enum VersionComparisonOperator {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The last two names clash with the others. I'd change the first four names so that they describe the functionality rather than the serialized form.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is actually surprisingly hard, particularly because ~ and ^ have a lower bound that isn't properly covered by the current name. Suggestions?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I changed the description for ~ and ^. I tried adjusting the other names, but it causes more confusion than sticking with the operators. The class itself represents the operators in the first place and has nothing to do with the user friendly translation that happens in ReasonAnalyzer.

import net.fabricmc.loader.api.metadata.version.VersionPredicate;
import net.fabricmc.loader.api.metadata.version.VersionPredicate.PredicateTerm;

final class ResultAnalyzer {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be nice to have tests for this.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

True, but not high priority. The class itself will likely see substantial changes, it is just a quick and dirty adaption of the old error generation logic.

@@ -167,8 +168,12 @@ public void injectIntoClassLoader(LaunchClassLoader launchClassLoader) {
}

@Override
public void propose(URL url) {
launchClassLoader.addURL(url);
public void addToClassPath(Path path) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ClassPath should be Classpath to be consistent with the typical capitalization both within Fabric loader and in general.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure about that actually, java started to separate it lately (--class-path instead of --classpath)..

@Technici4n
Copy link
Member

Looks generally good, however my loader knowledge is close to zero. I assume if there is an issue it will be easy to address.

Is some testing planned in case that happens?

@sfPlayer1
Copy link
Contributor Author

We want to do some manual testing with the Mixin changes before actually releasing 0.12, other than that there are only plans/ideas for making Loader more testable. I already did some local testing ofc.

@Technici4n
Copy link
Member

Yes ofc, I was referring to automated or at least semi-automated testing.

ModCandidate candidate = new ModCandidate(i, normalizedUrl, depth, requiresRemap);
boolean added;
if (!metadata.loadsInEnvironment(envType)) {
return null;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looking at #375, should we add a warning here? Though if a mod is not loaded, it won't appear in loader's mod list, and should be quite clear in that case.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The mod explicitly opted out of being loaded, doesn't look warning worthy to me?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, now I am thinking about using the 5-tier dependency system to handle the environment thing: a mod can declare "breaks" to strictly crash when loading on one side and "conflicts" for less-strict warning.

In fact, I think we can add an extra minecraft client builtin mod; the whole dedicated server is shipped within the client, so it will just count as part of minecraft builtin mod. If a mod opts-in to be client-only, it can declare a hard dependency on client mod; otherwise, it can declare a hard breaks on client (though I see no point for a mod to do so)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, @Chocohead what's the main problems with this?

Now, after some thinking, I feel such a split by using jij for client-specific code is actually a good approach, somewhat more feasible than adding @Environment annotations and having less compile-time guarantees

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One would just declare the environment in the mod json instead of messing with dependencies

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we should be encouraging/offering people to crash if their mod is on the wrong side. Right now we avoid loading it which is a much more user friendly approach to the problem. Forge in comparison expects mods to handle it themselves (unlike in the past with the old clientOnlyMod) but also doesn't offer a way to crash on the wrong side.

Copy link
Contributor

@liach liach left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks quite good in general. Hope you can commit the change that actually ships sat4j wholesale so people can build and test it more easily

@sfPlayer1 sfPlayer1 merged commit 8879128 into FabricMC:master Jul 3, 2021
@EZForever EZForever mentioned this pull request Jul 24, 2021
19 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

7 participants