Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/main/java/me/itzg/helpers/McImageHelper.java
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@
public class McImageHelper {

public static final String OPTION_SPLIT_COMMAS = "\\s*,\\s*";
public static final String VERSION_REGEX = "\\d+(\\.\\d+)+";

@SuppressWarnings("unused")
@CommandLine.Option(names = {"-h",
Expand Down
13 changes: 9 additions & 4 deletions src/main/java/me/itzg/helpers/errors/ExceptionHandler.java
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,15 @@ public int handleExecutionException(Exception e, CommandLine commandLine,
ParseResult parseResult) {

if (!mcImageHelper.isSilent()) {
log.error("'{}' command failed. Version is {}",
commandLine.getCommandName(),
McImageHelper.getVersion(),
e);
if (e instanceof InvalidParameterException) {
log.error("Invalid parameter provided for '{}' command: {}", commandLine.getCommandName(), e.getMessage());
log.debug("Invalid parameter details", e);
} else {
log.error("'{}' command failed. Version is {}",
commandLine.getCommandName(),
McImageHelper.getVersion(),
e);
}
}

final IExitCodeExceptionMapper mapper = commandLine.getExitCodeExceptionMapper();
Expand Down
81 changes: 56 additions & 25 deletions src/main/java/me/itzg/helpers/forge/ForgeInstaller.java
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
import java.time.Instant;
import java.util.Collections;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Objects;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
Expand All @@ -22,6 +23,7 @@
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import me.itzg.helpers.errors.GenericException;
import me.itzg.helpers.errors.InvalidParameterException;
import me.itzg.helpers.files.Manifests;
import me.itzg.helpers.files.ResultsFileWriter;
import me.itzg.helpers.forge.model.PromotionsSlim;
Expand Down Expand Up @@ -52,15 +54,23 @@ public void install(String minecraftVersion, String forgeVersion,
throw new GenericException("Failed to load existing forge manifest", e);
}

final PromotionsSlim promotionsSlim = loadPromotions();
if (promotionsSlim.getPromos().isEmpty()) {
throw new GenericException("No versions were available in Forge promotions");
}

final String resolvedMinecraftVersion;
final String resolvedForgeVersion;
if (forgeInstaller == null) {
resolvedMinecraftVersion = resolveMinecraftVersion(minecraftVersion, promotionsSlim);
try {
resolvedForgeVersion = resolveForgeVersion(minecraftVersion, forgeVersion);
resolvedForgeVersion = resolveForgeVersion(resolvedMinecraftVersion, forgeVersion, promotionsSlim);
} catch (IOException e) {
throw new RuntimeException("Failed to resolve forge version", e);
}
}
else {
resolvedMinecraftVersion = minecraftVersion;
resolvedForgeVersion = forgeInstaller.toString();
}

Expand All @@ -76,17 +86,17 @@ else if (prevManifest != null) {
needsInstall = true;
}
else if (
Objects.equals(prevManifest.getMinecraftVersion(), minecraftVersion) &&
Objects.equals(prevManifest.getMinecraftVersion(), resolvedMinecraftVersion) &&
Objects.equals(prevManifest.getForgeVersion(), resolvedForgeVersion)
) {
log.info("Forge version {} for minecraft version {} is already installed",
resolvedForgeVersion, minecraftVersion
resolvedForgeVersion, resolvedMinecraftVersion
);
needsInstall = false;
} else {
log.info("Re-installing Forge due to version change from MC {}/Forge {} to MC {}/Forge {}",
prevManifest.getMinecraftVersion(), prevManifest.getForgeVersion(),
minecraftVersion, resolvedForgeVersion);
resolvedMinecraftVersion, resolvedForgeVersion);
needsInstall = true;
}
}
Expand All @@ -97,10 +107,10 @@ else if (
final ForgeManifest newManifest;
if (needsInstall) {
if (forgeInstaller == null) {
newManifest = downloadAndInstall(minecraftVersion, resolvedForgeVersion, outputDir);
newManifest = downloadAndInstall(resolvedMinecraftVersion, resolvedForgeVersion, outputDir);
}
else {
newManifest = installUsingExisting(minecraftVersion, resolvedForgeVersion, outputDir, forgeInstaller);
newManifest = installUsingExisting(resolvedMinecraftVersion, resolvedForgeVersion, outputDir, forgeInstaller);
}

Manifests.save(outputDir, MANIFEST_ID, newManifest);
Expand Down Expand Up @@ -304,7 +314,7 @@ private Path downloadInstaller(Path outputDir, String minecraftVersion, String f
}

if (!success) {
throw new RuntimeException("Failed to locate forge installer");
throw new GenericException("Failed to locate forge installer");
}

return installerJar;
Expand All @@ -319,25 +329,14 @@ static class PromoEntry {
String forgeVersion;
}

private String resolveForgeVersion(String minecraftVersion, String forgeVersion) throws IOException {
private String resolveForgeVersion(String minecraftVersion, String forgeVersion, PromotionsSlim promotionsSlim) throws IOException {
final String normalized = forgeVersion.toLowerCase();
if (!normalized.equals(LATEST) && !normalized.equals(RECOMMENDED)) {
return forgeVersion;
}

final PromotionsSlim promotionsSlim =
fetch(URI.create("https://files.minecraftforge.net/maven/net/minecraftforge/forge/promotions_slim.json"))
.userAgentCommand("forge")
.toObject(PromotionsSlim.class)
.execute();

final Map<String, String> options = promotionsSlim.getPromos().entrySet().stream()
// each entry is like
// "1.19-recommended": "41.1.0"
.map(entry -> {
final String[] keyParts = entry.getKey().split("-", 2);
return new PromoEntry(keyParts[0], keyParts[1].toLowerCase(), entry.getValue());
})
.map(ForgeInstaller::parsePromoEntry)
// narrow to just applicable minecraft version
.filter(entry -> entry.getMcVersion().equals(minecraftVersion))
// ...and arrive at a map that has one or two entries for latest and/or recommended
Expand All @@ -348,12 +347,44 @@ private String resolveForgeVersion(String minecraftVersion, String forgeVersion)

log.debug("Narrowed forge versions to {} and looking for {}", options, normalized);

final String result = options.get(normalized);
if (result != null) {
return result;
if (!options.isEmpty()) {
final String result = options.get(normalized);
if (result != null) {
return result;
} else {
// ...otherwise need to fall back to what we have
return options.values().iterator().next();
}
}
else {
throw new InvalidParameterException(String.format("Minecraft version %s not available from Forge", minecraftVersion));
}
}

private static PromotionsSlim loadPromotions() {
return fetch(URI.create("https://files.minecraftforge.net/maven/net/minecraftforge/forge/promotions_slim.json"))
.userAgentCommand("forge")
.toObject(PromotionsSlim.class)
.execute();
}

private String resolveMinecraftVersion(String minecraftVersion, PromotionsSlim promotionsSlim) {
if (minecraftVersion.equalsIgnoreCase(LATEST)) {
return promotionsSlim.getPromos().entrySet().stream()
.map(ForgeInstaller::parsePromoEntry)
// pick off the last entry, where order is significant since JSON parsing retains ordering
.reduce((lhs, rhs) -> rhs)
.map(promoEntry -> promoEntry.mcVersion)
.orElseThrow(() -> new GenericException("No versions were available in Forge promotions"));
} else {
// ...otherwise need to fall back to what we have
return options.values().iterator().next();
return minecraftVersion;
}
}

private static PromoEntry parsePromoEntry(Entry<String, String> entry) {
// each entry is like
// "1.19-recommended": "41.1.0"
final String[] keyParts = entry.getKey().split("-", 2);
return new PromoEntry(keyParts[0], keyParts[1].toLowerCase(), entry.getValue());
}
}
23 changes: 21 additions & 2 deletions src/main/java/me/itzg/helpers/forge/InstallForgeCommand.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package me.itzg.helpers.forge;

import static me.itzg.helpers.McImageHelper.VERSION_REGEX;

import java.nio.file.Path;
import java.util.concurrent.Callable;
import java.util.regex.Pattern;
Expand All @@ -13,15 +15,32 @@

@Command(name = "install-forge", description = "Downloads and installs a requested version of Forge")
public class InstallForgeCommand implements Callable<Integer> {
@Spec
CommandLine.Model.CommandSpec spec;

@Option(names = {"--help","-h"}, usageHelp = true)
boolean help;

public static final Pattern ALLOWED_MINECRAFT_VERSION = Pattern.compile(
String.join("|", ForgeInstaller.LATEST, VERSION_REGEX),
Pattern.CASE_INSENSITIVE
);

public static final Pattern ALLOWED_FORGE_VERSION = Pattern.compile(
String.join("|", ForgeInstaller.LATEST, ForgeInstaller.RECOMMENDED, "\\d+(\\.\\d+)+"),
String.join("|", ForgeInstaller.LATEST, ForgeInstaller.RECOMMENDED, VERSION_REGEX),
Pattern.CASE_INSENSITIVE
);

@Option(names = "--minecraft-version", required = true)
@Option(names = "--minecraft-version", required = true, paramLabel = "VERSION",
defaultValue = "latest",
description = "'latest', which is the default, or a specific version"
)
public void setMinecraftVersion(String minecraftVersion) {
if (!ALLOWED_MINECRAFT_VERSION.matcher(minecraftVersion).matches()) {
throw new ParameterException(spec.commandLine(), "Invalid value for minecraft version: " + minecraftVersion);
}
this.minecraftVersion = minecraftVersion;
}
String minecraftVersion;

static class VersionOrInstaller {
Expand Down