-
-
Notifications
You must be signed in to change notification settings - Fork 26
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
modrinth: Add support for downloading and installing informal modrint…
…h packs (#272)
- Loading branch information
Showing
13 changed files
with
1,092 additions
and
309 deletions.
There are no files selected for viewing
337 changes: 43 additions & 294 deletions
337
src/main/java/me/itzg/helpers/modrinth/InstallModrinthModpackCommand.java
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
76 changes: 76 additions & 0 deletions
76
src/main/java/me/itzg/helpers/modrinth/ModrinthApiPackFetcher.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,76 @@ | ||
package me.itzg.helpers.modrinth; | ||
|
||
import java.nio.file.Path; | ||
|
||
import lombok.extern.slf4j.Slf4j; | ||
import me.itzg.helpers.errors.*; | ||
import me.itzg.helpers.files.Manifests; | ||
import me.itzg.helpers.http.FailedRequestException; | ||
import me.itzg.helpers.modrinth.model.*; | ||
import reactor.core.publisher.Mono; | ||
|
||
@Slf4j | ||
public class ModrinthApiPackFetcher implements ModrinthPackFetcher { | ||
private ModrinthApiClient apiClient; | ||
|
||
private ProjectRef modpackProjectRef; | ||
|
||
private Loader modLoaderType; | ||
private String gameVersion; | ||
private VersionType defaultVersionType; | ||
private Path modpackOutputDirectory; | ||
|
||
ModrinthApiPackFetcher( | ||
ModrinthApiClient apiClient, ProjectRef projectRef, | ||
Path outputDirectory, String gameVersion, | ||
VersionType defaultVersionType, Loader loader) | ||
{ | ||
this.apiClient = apiClient; | ||
this.modpackProjectRef = projectRef; | ||
this.modpackOutputDirectory = outputDirectory; | ||
this.gameVersion = gameVersion; | ||
this.defaultVersionType = defaultVersionType; | ||
this.modLoaderType = loader; | ||
} | ||
|
||
public Mono<Path> fetchModpack(ModrinthModpackManifest prevManifest) { | ||
return this.resolveModpackVersion() | ||
.filter(version -> needsInstall(prevManifest, version)) | ||
.flatMap(version -> | ||
Mono.just(ModrinthApiClient.pickVersionFile(version))) | ||
.flatMap(versionFile -> apiClient.downloadMrPack(versionFile)); | ||
} | ||
|
||
private Mono<Version> resolveModpackVersion() { | ||
return this.apiClient.getProject(this.modpackProjectRef.getIdOrSlug()) | ||
.onErrorMap(FailedRequestException::isNotFound, | ||
throwable -> | ||
new InvalidParameterException( | ||
"Unable to locate requested project given " + | ||
this.modpackProjectRef.getIdOrSlug(), throwable)) | ||
.flatMap(project -> | ||
this.apiClient.resolveProjectVersion( | ||
project, this.modpackProjectRef, this.modLoaderType, | ||
this.gameVersion, this.defaultVersionType) | ||
); | ||
} | ||
|
||
private boolean needsInstall( | ||
ModrinthModpackManifest prevManifest, Version version) | ||
{ | ||
if (prevManifest != null) { | ||
if (prevManifest.getProjectSlug().equals(version.getProjectId()) | ||
&& prevManifest.getVersionId().equals(version.getId()) | ||
&& prevManifest.getDependencies() != null | ||
&& Manifests.allFilesPresent( | ||
modpackOutputDirectory, prevManifest) | ||
) { | ||
log.info("Modpack {} version {} is already installed", | ||
version.getProjectId(), version.getName() | ||
); | ||
return false; | ||
} | ||
} | ||
return true; | ||
} | ||
} |
28 changes: 28 additions & 0 deletions
28
src/main/java/me/itzg/helpers/modrinth/ModrinthHttpPackFetcher.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
package me.itzg.helpers.modrinth; | ||
|
||
import java.net.URI; | ||
import java.nio.file.Path; | ||
|
||
import lombok.extern.slf4j.Slf4j; | ||
import reactor.core.publisher.Mono; | ||
|
||
@Slf4j | ||
public class ModrinthHttpPackFetcher implements ModrinthPackFetcher { | ||
private final ModrinthApiClient apiClient; | ||
private final Path destFilePath; | ||
private final URI modpackUri; | ||
|
||
ModrinthHttpPackFetcher(ModrinthApiClient apiClient, Path basePath, URI uri) { | ||
this.apiClient = apiClient; | ||
this.destFilePath = basePath.resolve("modpack.mrpack"); | ||
this.modpackUri = uri; | ||
} | ||
|
||
@Override | ||
public Mono<Path> fetchModpack(ModrinthModpackManifest prevManifest) { | ||
return this.apiClient.downloadFileFromUrl( | ||
this.destFilePath, this.modpackUri, | ||
(uri, file, contentSizeBytes) -> | ||
log.info("Downloaded {}", this.destFilePath)); | ||
} | ||
} |
9 changes: 9 additions & 0 deletions
9
src/main/java/me/itzg/helpers/modrinth/ModrinthPackFetcher.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
package me.itzg.helpers.modrinth; | ||
|
||
import java.nio.file.Path; | ||
|
||
import reactor.core.publisher.Mono; | ||
|
||
public interface ModrinthPackFetcher { | ||
Mono<Path> fetchModpack(ModrinthModpackManifest prevManifest); | ||
} |
222 changes: 222 additions & 0 deletions
222
src/main/java/me/itzg/helpers/modrinth/ModrinthPackInstaller.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,222 @@ | ||
package me.itzg.helpers.modrinth; | ||
|
||
import java.io.IOException; | ||
import java.nio.file.Files; | ||
import java.nio.file.Path; | ||
import java.nio.file.StandardCopyOption; | ||
import java.util.List; | ||
import java.util.Map; | ||
import java.util.Objects; | ||
import java.util.function.Function; | ||
import java.util.stream.Collectors; | ||
import java.util.stream.Stream; | ||
import java.util.zip.ZipFile; | ||
|
||
import lombok.Data; | ||
import lombok.extern.slf4j.Slf4j; | ||
import me.itzg.helpers.errors.GenericException; | ||
import me.itzg.helpers.errors.InvalidParameterException; | ||
import me.itzg.helpers.fabric.FabricLauncherInstaller; | ||
import me.itzg.helpers.files.IoStreams; | ||
import me.itzg.helpers.forge.ForgeInstaller; | ||
import me.itzg.helpers.http.SharedFetch.Options; | ||
import me.itzg.helpers.json.ObjectMappers; | ||
import me.itzg.helpers.modrinth.model.*; | ||
import me.itzg.helpers.quilt.QuiltInstaller; | ||
import reactor.core.publisher.Flux; | ||
import reactor.core.publisher.Mono; | ||
import reactor.core.scheduler.Schedulers; | ||
|
||
@Slf4j | ||
public class ModrinthPackInstaller { | ||
private final ModrinthApiClient apiClient; | ||
private final Path zipFile; | ||
private final Path outputDirectory; | ||
private final Path resultsFile; | ||
private final boolean forceModloaderReinstall; | ||
private final Options sharedFetchOpts; | ||
|
||
public ModrinthPackInstaller( | ||
ModrinthApiClient apiClient, Options sharedFetchOpts, | ||
Path zipFile, Path outputDirectory, Path resultsFile, | ||
boolean forceModloaderReinstall) | ||
{ | ||
this.apiClient = apiClient; | ||
this.sharedFetchOpts = sharedFetchOpts; | ||
this.zipFile = zipFile; | ||
this.outputDirectory = outputDirectory; | ||
this.resultsFile = resultsFile; | ||
this.forceModloaderReinstall = forceModloaderReinstall; | ||
} | ||
|
||
public Mono<Installation> processModpack() { | ||
final ModpackIndex modpackIndex; | ||
try { | ||
modpackIndex = IoStreams.readFileFromZip( | ||
this.zipFile, "modrinth.index.json", in -> | ||
ObjectMappers.defaultMapper().readValue(in, ModpackIndex.class) | ||
); | ||
} catch (IOException e) { | ||
return Mono.error( | ||
new GenericException("Failed to read modpack index", e)); | ||
} | ||
|
||
if (modpackIndex == null) { | ||
return Mono.error( | ||
new InvalidParameterException( | ||
"Modpack is missing modrinth.index.json") | ||
); | ||
} | ||
|
||
if (!Objects.equals("minecraft", modpackIndex.getGame())) { | ||
return Mono.error( | ||
new InvalidParameterException( | ||
"Requested modpack is not for minecraft: " + | ||
modpackIndex.getGame())); | ||
} | ||
|
||
return processModpackFiles(modpackIndex) | ||
.collectList() | ||
.map(modFiles -> | ||
Stream.of( | ||
modFiles.stream(), | ||
extractOverrides("overrides", "server-overrides") | ||
) | ||
.flatMap(Function.identity()) | ||
.collect(Collectors.toList()) | ||
) | ||
.flatMap(paths -> { | ||
try { | ||
applyModLoader(modpackIndex.getDependencies()); | ||
} catch (IOException e) { | ||
return Mono.error( | ||
new GenericException("Failed to apply mod loader", e)); | ||
} | ||
|
||
return Mono.just(new Installation() | ||
.setIndex(modpackIndex) | ||
.setFiles(paths)); | ||
}); | ||
} | ||
|
||
private Flux<Path> processModpackFiles(ModpackIndex modpackIndex) { | ||
return Flux.fromStream(modpackIndex.getFiles().stream() | ||
.filter(modpackFile -> | ||
// env is optional | ||
modpackFile.getEnv() == null | ||
|| modpackFile.getEnv() | ||
.get(Env.server) != EnvType.unsupported | ||
) | ||
) | ||
.publishOn(Schedulers.boundedElastic()) | ||
.flatMap(modpackFile -> { | ||
final Path outFilePath = | ||
this.outputDirectory.resolve(modpackFile.getPath()); | ||
try { | ||
//noinspection BlockingMethodInNonBlockingContext | ||
Files.createDirectories(outFilePath.getParent()); | ||
} catch (IOException e) { | ||
return Mono.error(new GenericException( | ||
"Failed to created directory for file to download", e)); | ||
} | ||
|
||
return this.apiClient.downloadFileFromUrl( | ||
outFilePath, | ||
modpackFile.getDownloads().get(0), | ||
(uri, file, contentSizeBytes) -> | ||
log.info("Downloaded {}", modpackFile.getPath()) | ||
); | ||
}); | ||
} | ||
|
||
@SuppressWarnings("SameParameterValue") | ||
private Stream<Path> extractOverrides(String... overridesDirs) { | ||
try (ZipFile zipFileReader = new ZipFile(zipFile.toFile())) { | ||
return Stream.of(overridesDirs) | ||
.flatMap(dir -> { | ||
final String prefix = dir + "/"; | ||
return zipFileReader.stream() | ||
.filter(entry -> !entry.isDirectory() | ||
&& entry.getName().startsWith(prefix) | ||
) | ||
.map(entry -> { | ||
final Path outFile = outputDirectory.resolve( | ||
entry.getName().substring(prefix.length()) | ||
); | ||
|
||
try { | ||
Files.createDirectories(outFile.getParent()); | ||
Files.copy(zipFileReader.getInputStream(entry), outFile, StandardCopyOption.REPLACE_EXISTING); | ||
return outFile; | ||
} catch (IOException e) { | ||
throw new GenericException( | ||
String.format("Failed to extract %s from overrides", entry.getName()), e | ||
); | ||
} | ||
}); | ||
}) | ||
// need to eager load the stream while the zip file is open | ||
.collect(Collectors.toList()) | ||
.stream(); | ||
} catch (IOException e) { | ||
throw new GenericException("Failed to extract overrides", e); | ||
} | ||
} | ||
|
||
private void applyModLoader( | ||
Map<DependencyId, String> dependencies | ||
) throws IOException | ||
{ | ||
log.debug("Applying mod loader from dependencies={}", dependencies); | ||
|
||
final String minecraftVersion = dependencies.get(DependencyId.minecraft); | ||
if (minecraftVersion == null) { | ||
throw new GenericException( | ||
"Modpack dependencies missing minecraft version: " + dependencies); | ||
} | ||
|
||
final String forgeVersion = dependencies.get(DependencyId.forge); | ||
if (forgeVersion != null) { | ||
new ForgeInstaller().install( | ||
minecraftVersion, | ||
forgeVersion, | ||
this.outputDirectory, | ||
this.resultsFile, | ||
this.forceModloaderReinstall, | ||
null | ||
); | ||
return; | ||
} | ||
|
||
final String fabricVersion = dependencies.get(DependencyId.fabricLoader); | ||
if (fabricVersion != null) { | ||
new FabricLauncherInstaller(this.outputDirectory) | ||
.setResultsFile(this.resultsFile) | ||
.installUsingVersions( | ||
minecraftVersion, | ||
fabricVersion, | ||
null | ||
); | ||
return; | ||
} | ||
|
||
final String quiltVersion = dependencies.get(DependencyId.quiltLoader); | ||
if (quiltVersion != null) { | ||
try (QuiltInstaller installer = | ||
new QuiltInstaller(QuiltInstaller.DEFAULT_REPO_URL, | ||
this.sharedFetchOpts, | ||
this.outputDirectory, | ||
minecraftVersion) | ||
.setResultsFile(this.resultsFile)) { | ||
|
||
installer.installWithVersion(null, quiltVersion); | ||
} | ||
} | ||
} | ||
|
||
@Data | ||
class Installation { | ||
ModpackIndex index; | ||
List<Path> files; | ||
} | ||
} |
Oops, something went wrong.