Skip to content

Commit

Permalink
Enhanced asset download support (#25)
Browse files Browse the repository at this point in the history
- Add support for copying Assets from an existing Launcher
- Add support for writing the Neoform asset JSON reference format
- Add support for specifying the asset download directory
  • Loading branch information
shartte authored Jul 19, 2024
1 parent 7982dab commit 4b7030b
Show file tree
Hide file tree
Showing 24 changed files with 1,281 additions and 217 deletions.
15 changes: 12 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,13 +69,13 @@ NFRT helps with this by downloading the assets required to run a particular vers

```
# Download Assets for a specific version of Minecraft
nfrt download-assets --minecraft-version 1.20.6 --output-properties-to assets.properties
nfrt download-assets --minecraft-version 1.20.6 --write-properties assets.properties
# Download Assets for the Minecraft version used by the given NeoForm version
nfrt download-assets --neoform net.neoforged:neoform:1.20.6-20240429.153634@zip --output-properties-to assets.properties
nfrt download-assets --neoform net.neoforged:neoform:1.20.6-20240429.153634@zip --write-properties assets.properties
# Download Assets for the Minecraft version used by the given NeoForge version
nfrt download-assets --neoforge net.neoforged:neoforge:20.6.72-beta:userdev --output-properties-to assets.properties
nfrt download-assets --neoforge net.neoforged:neoforge:20.6.72-beta:userdev --write-properties assets.properties
# In all three cases, a properties file will be written to assets.properties containing the following,
# which can be used to pass the required command line arguments for starting a Minecraft client.
Expand All @@ -86,6 +86,15 @@ assets_root=...path to assets...
While it may seem odd that NFRT supports passing NeoForm or NeoForge versions to this command, this is in service
of potential Gradle plugins never having to actually read and parse the NeoForm configuration file.

| Option | Description |
|--------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `--asets-dir` | Where to store the downloaded assets. Optional. Defaults to `<nfrt_home>/assets`, or a detected Launcher installation. |
| `--no-copy-launcher-assets` | Disables copying of local Minecraft Launcher assets, if using the asset root directly is disabled. |
| `--no-use-launcher-asset-root` | Disables using a detected Minecraft Launcher installation directly to store the required assets. |
| `--concurrent-downloads` | Limits the maximum number of concurrent downloads. Default is 25. |
| `--write-properties` | Writes a property file to the given path that contains the asset index id (`asset_index`) and asset root path (`assets_root`) suitable for passing to Minecraft. |
| `--write-json` | Writes a JSON file to the given path that contains the asset index id (`asset_index`) and asset root path (`assets`) suitable for passing to Minecraft via a Neoform entrypoint. |

## Common Options

These options affect all NFRT subcommands.
Expand Down
6 changes: 5 additions & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -101,8 +101,12 @@ dependencies {
annotationProcessor 'info.picocli:picocli-codegen:4.7.6'

testImplementation platform('org.junit:junit-bom:5.10.0')
testImplementation platform('org.mockito:mockito-bom:5.12.0')
testImplementation platform('org.assertj:assertj-bom:3.26.3')
testImplementation 'org.junit.jupiter:junit-jupiter'
testImplementation 'org.junit.jupiter:junit-jupiter-params'
testImplementation 'org.mockito:mockito-junit-jupiter'
testImplementation 'org.assertj:assertj-core'
}

// Add dependencies to external tools
Expand Down Expand Up @@ -203,7 +207,7 @@ idea {
}
"Download assets 1.20.6"(Application) {
mainClass = mainClassName
programParameters = "download-assets --neoforge net.neoforged:neoforge:20.6.72-beta:userdev --output-properties-to build/assets.properties"
programParameters = "download-assets --neoforge net.neoforged:neoforge:20.6.72-beta:userdev --write-properties build/assets.properties --write-json build/assets.json"
moduleRef(project, sourceSets.main)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@

import net.neoforged.neoform.runtime.cache.CacheKeyBuilder;
import net.neoforged.neoform.runtime.engine.ProcessingEnvironment;
import net.neoforged.neoform.runtime.graph.ResultRepresentation;
import net.neoforged.neoform.runtime.graph.ExecutionNodeAction;
import net.neoforged.neoform.runtime.graph.ResultRepresentation;

import java.io.IOException;
import java.nio.file.Files;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package net.neoforged.neoform.runtime.actions;

import net.neoforged.neoform.runtime.cache.CacheKeyBuilder;
import net.neoforged.neoform.runtime.artifacts.ArtifactManager;
import net.neoforged.neoform.runtime.cache.CacheKeyBuilder;
import net.neoforged.neoform.runtime.engine.ProcessingEnvironment;
import net.neoforged.neoform.runtime.graph.ResultRepresentation;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
import net.neoforged.neoform.runtime.graph.ExecutionNodeBuilder;
import net.neoforged.neoform.runtime.graph.NodeOutput;
import net.neoforged.neoform.runtime.graph.NodeOutputType;
import net.neoforged.neoform.runtime.utils.MavenCoordinate;
import net.neoforged.neoform.runtime.utils.ToolCoordinate;

import java.nio.file.Path;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
package net.neoforged.neoform.runtime.artifacts;

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.attribute.BasicFileAttributes;

public record Artifact(Path path, long lastModified, long size) {
public static Artifact ofPath(Path path) throws IOException {
var attributes = Files.readAttributes(path, BasicFileAttributes.class);
return new Artifact(path, attributes.lastModifiedTime().toMillis(), attributes.size());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ public Artifact get(MinecraftLibrary library) throws IOException {
// Ensure the file matches before using it
var fileHash = HashingUtil.hashFile(localPath, artifact.checksumAlgorithm());
if (Objects.equals(fileHash, artifact.checksum())) {
return getArtifactFromPath(localPath);
return Artifact.ofPath(localPath);
}
} catch (IOException ignored) {
// Ignore if it doesn't exist or is otherwise fails to be read
Expand Down Expand Up @@ -175,7 +175,7 @@ public Artifact getVersionManifest(String minecraftVersion) throws IOException {
for (var root : launcherInstallations.getInstallationRoots()) {
var localPath = root.resolve("versions").resolve(minecraftVersion).resolve(minecraftVersion + ".json");
if (Files.isReadable(localPath)) {
return getArtifactFromPath(localPath);
return Artifact.ofPath(localPath);
}
}

Expand Down Expand Up @@ -250,15 +250,7 @@ public void loadArtifactManifest(Path artifactManifestPath) throws IOException {
}

private Artifact getArtifactFromPath(String path) throws IOException {
return getArtifactFromPath(Paths.get(path));
}

private Artifact getArtifactFromPath(Path path) throws IOException {
if (!Files.isRegularFile(path)) {
throw new NoSuchFileException(path.toString());
}
var attrView = Files.getFileAttributeView(path, BasicFileAttributeView.class).readAttributes();
return new Artifact(path, attrView.lastModifiedTime().toMillis(), attrView.size());
return Artifact.ofPath(Paths.get(path));
}

@FunctionalInterface
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,12 +73,12 @@ public class CacheManager implements AutoCloseable {
private boolean analyzeMisses;
private boolean verbose;

public CacheManager(Path homeDir, Path workspacesDir) throws IOException {
public CacheManager(Path homeDir, @Nullable Path assetsDir, Path workspacesDir) throws IOException {
this.homeDir = homeDir;
Files.createDirectories(homeDir);
this.artifactCacheDir = homeDir.resolve("artifacts");
this.intermediateResultsDir = homeDir.resolve("intermediate_results");
this.assetsDir = homeDir.resolve("assets");
this.assetsDir = Objects.requireNonNullElse(assetsDir, homeDir.resolve("assets"));
this.workspacesDir = workspacesDir;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,16 @@
import net.neoforged.neoform.runtime.utils.Logger;
import org.jetbrains.annotations.Nullable;

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.NoSuchFileException;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

Expand Down Expand Up @@ -39,19 +44,23 @@ public class LauncherInstallations {
};
private static final Pattern PLACEHOLDER_PATTERN = Pattern.compile("\\$\\{([^}]+)}");

// Sort launcher directories in descending order by the number of asset indices they contain
private static final Comparator<LauncherDirectory> ASSET_INDEX_COUNT_DESCENDING = Comparator.<LauncherDirectory>comparingInt(d -> d.assetIndices.size()).reversed();

private final List<LauncherDirectory> launcherDirectories = new ArrayList<>();

/** If true, the scan was already performed and launcherDirectories is up-to-date */
/** If true, the scan was already performed and launcherDirectories is up to date */
private boolean scanned;

private boolean verbose;

public LauncherInstallations(List<Path> additionalLauncherDirs) {
public LauncherInstallations(List<Path> additionalLauncherDirs) throws IOException {
for (var dir : additionalLauncherDirs) {
var launcherDir = analyzeLauncherDirectory(dir);
if (launcherDir != null) {
launcherDirectories.add(launcherDir);
if (launcherDir == null) {
throw new NoSuchFileException(dir.toString());
}
launcherDirectories.add(launcherDir);
}
}

Expand All @@ -70,6 +79,51 @@ public List<Path> getInstallationRoots() {
return launcherDirectories.stream().map(LauncherDirectory::directory).toList();
}

/**
* Try finding an asset root in any of the known launcher installations that already has
* an asset index with the given id.
*
* @return Null if no such launcher installation could be found.
*/
@Nullable
public Path getAssetDirectoryForIndex(String assetIndexId) {
scanIfNecessary();

var haveIndex = new ArrayList<LauncherDirectory>();
for (var launcherDirectory : launcherDirectories) {
if (launcherDirectory.assetIndices.contains(assetIndexId)) {
haveIndex.add(launcherDirectory);
}
}

// Sort by count of other indices descending
haveIndex.sort(ASSET_INDEX_COUNT_DESCENDING);

if (!haveIndex.isEmpty()) {
return haveIndex.getFirst().assetDirectory();
}

return null;
}

/**
* Returns the list of asset directories found among the known launcher installations.
*/
public List<Path> getAssetRoots() {
scanIfNecessary();

var launcherDirsWithAssets = new ArrayList<LauncherDirectory>();
for (var launcherDirectory : launcherDirectories) {
if (launcherDirectory.assetDirectory() != null) {
launcherDirsWithAssets.add(launcherDirectory);
}
}

launcherDirsWithAssets.sort(ASSET_INDEX_COUNT_DESCENDING);

return launcherDirsWithAssets.stream().map(LauncherDirectory::assetDirectory).toList();
}

private void scanIfNecessary() {
if (scanned) {
return;
Expand Down Expand Up @@ -112,8 +166,15 @@ private void scanIfNecessary() {
if (verbose) {
LOG.println("Launcher directories found:");
for (var launcherDirectory : launcherDirectories) {
String details;
if (launcherDirectory.assetDirectory == null || launcherDirectory.assetIndices().isEmpty()) {
details = "no assets";
} else {
details = "asset indices: " + String.join(" ", launcherDirectory.assetIndices());
}

LOG.println(AnsiColor.MUTED + " " + launcherDirectory.directory
+ AnsiColor.RESET);
+ " (" + details + ")" + AnsiColor.RESET);
}
}
}
Expand Down Expand Up @@ -148,17 +209,34 @@ private String resolvePlaceholders(String candidate) {
}

@Nullable
private LauncherDirectory analyzeLauncherDirectory(Path installDir) {
private LauncherDirectory analyzeLauncherDirectory(Path installDir) throws IOException {
if (!Files.isDirectory(installDir)) {
if (verbose) {
LOG.println(AnsiColor.MUTED + " Not found: " + installDir + AnsiColor.RESET);
}
return null;
}

return new LauncherDirectory(installDir);
var assetIndices = new HashSet<String>();
var assetRoot = installDir.resolve("assets");
var assetIndicesDir = assetRoot.resolve("indexes");
var assetObjectsDir = assetRoot.resolve("objects");
if (Files.isDirectory(assetIndicesDir) && Files.isDirectory(assetObjectsDir)) {
// Count the number of asset indices present to judge how viable this directory is
try (var stream = Files.list(assetIndicesDir)) {
stream.map(f -> f.getFileName().toString())
.filter(f -> f.endsWith(".json"))
.map(f -> f.substring(0, f.length() - 5))
.forEach(assetIndices::add);
}
}
if (assetIndices.isEmpty()) {
assetRoot = null; // Do not use an asset root, when it is empty
}

return new LauncherDirectory(installDir, assetRoot, assetIndices);
}

private record LauncherDirectory(Path directory) {
private record LauncherDirectory(Path directory, @Nullable Path assetDirectory, Set<String> assetIndices) {
}
}
Loading

0 comments on commit 4b7030b

Please sign in to comment.