Skip to content

Commit

Permalink
Make Vanilla-Mode (NeoForm only) work (#16)
Browse files Browse the repository at this point in the history
  • Loading branch information
shartte authored Jun 9, 2024
1 parent 1e1646f commit 4f5eae6
Show file tree
Hide file tree
Showing 8 changed files with 173 additions and 49 deletions.
118 changes: 80 additions & 38 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
- Supports the [Gradle configuration cache](https://docs.gradle.org/current/userguide/configuration_cache.html) to speed
up repeated runs of Gradle tasks

## Basic Usage
## Basic Usage for NeoForge Mods

In `gradle.properties`:

Expand Down Expand Up @@ -77,6 +77,42 @@ neoForge {

See the example code in [the test project](./testproject/build.gradle).

## Vanilla-Mode

In multi-loader projects, you'll often need a subproject for your cross-loader code. This project will also need
access to Minecraft classes, but without any loader-specific extensions.

This plugin solves that by offering a "Vanilla-mode" which you enable by
specifying a [NeoForm version](https://projects.neoforged.net/neoforged/neoform) instead of a NeoForge version.
NeoForm contains the necessary configuration to produce Minecraft jar-files that you can compile against
that contain no other modifications.

In Vanilla-mode, only the `client`, `server` and `data` run types are supported.
Since the plugin includes no mod loader code in this mode, only basic resource- and data packs will be usable in-game.

In `build.gradle`:

Apply the plugin as usual and use a configuration block like this:

```groovy
neoForge {
// Look for versions on https://projects.neoforged.net/neoforged/neoform
neoFormVersion = "1.20.6-20240524.210247"
runs {
client {
client()
}
server {
server()
}
data {
data()
}
}
}
```

## More Configuration

### Runs
Expand All @@ -89,37 +125,37 @@ The run type can be set as follows:
```groovy
neoForge {
runs {
<run name> {
< run name > {
// This is the standard syntax:
type = "gameTestServer"
// Client, data and server runs can use a shorthand instead:
// client()
// data()
// server()
// Add arguments passed to the main method
programArguments = ["--arg"]
programArgument("--arg")
// Add arguments passed to the JVM
jvmArguments = ["-XX:+AllowEnhancedClassRedefinition"]
jvmArgument("-XX:+AllowEnhancedClassRedefinition")
// Add system properties
systemProperties = [
"a.b.c": "xyz"
]
systemProperty("a.b.c", "xyz")
// Set or add environment variables
environment = [
"FOO_BAR": "123"
]
environment("FOO_BAR", "123")
// Optionally set the log-level used by the game
logLevel = org.slf4j.event.Level.DEBUG
// You can change the name used for this run in your IDE
ideName = "Run Game Tests"
}
Expand Down Expand Up @@ -157,21 +193,23 @@ Alternatively, you can set it in your build.gradle:

```groovy
neoForge {
// [...]
parchment {
// Get versions from https://parchmentmc.org/docs/getting-started
// Omit the "v"-prefix in mappingsVersion
minecraftVersion = "1.20.6"
mappingsVersion = "2024.05.01"
}
// [...]
parchment {
// Get versions from https://parchmentmc.org/docs/getting-started
// Omit the "v"-prefix in mappingsVersion
minecraftVersion = "1.20.6"
mappingsVersion = "2024.05.01"
}
}
```

### Unit testing with JUnit

On top of gametests, this plugin supports unit testing mods with JUnit.

For the minimal setup, add the following code to your build script:

```groovy
// Add a test dependency on the test engine JUnit
dependencies {
Expand All @@ -190,7 +228,7 @@ neoForge {
enable()
// Configure which mod is being tested.
// This allows NeoForge to load the test/ classes and resources as belonging to the mod.
testedMod = mods.<mod name> // <mod name> must match the name in the mods { } block.
testedMod = mods.<mod name > // <mod name> must match the name in the mods { } block.
}
}
```
Expand All @@ -199,15 +237,19 @@ You can now use the `@Test` annotation for your unit tests inside the `test/` fo
and reference Minecraft classes.

#### Loading a server

With the NeoForge test framework, you can run your unit tests in the context of a Minecraft server:

```groovy
dependencies {
testImplementation "net.neoforged:testframework:<neoforge version>"
}
```

With this dependency, you can annotate your test class as follows:

```java

@ExtendWith(EphemeralTestServerProvider.class)
public class TestClass {
@Test
Expand Down Expand Up @@ -236,26 +278,26 @@ configurations.all {

```groovy
neoForge {
neoFormRuntime {
// Use specific NFRT version
// Gradle Property: neoForge.neoFormRuntime.version
version = "1.2.3"
// Control use of cache
// Gradle Property: neoForge.neoFormRuntime.enableCache
enableCache = false
// Enable Verbose Output
// Gradle Property: neoForge.neoFormRuntime.verbose
verbose = true
// Use Eclipse Compiler for Minecraft
// Gradle Property: neoForge.neoFormRuntime.useEclipseCompiler
useEclipseCompiler = true
// Print more information when NFRT cannot use a cached result
// Gradle Property: neoForge.neoFormRuntime.analyzeCacheMisses
analyzeCacheMisses = true
}
neoFormRuntime {
// Use specific NFRT version
// Gradle Property: neoForge.neoFormRuntime.version
version = "1.2.3"
// Control use of cache
// Gradle Property: neoForge.neoFormRuntime.enableCache
enableCache = false
// Enable Verbose Output
// Gradle Property: neoForge.neoFormRuntime.verbose
verbose = true
// Use Eclipse Compiler for Minecraft
// Gradle Property: neoForge.neoFormRuntime.useEclipseCompiler
useEclipseCompiler = true
// Print more information when NFRT cannot use a cached result
// Gradle Property: neoForge.neoFormRuntime.analyzeCacheMisses
analyzeCacheMisses = true
}
}
```
Original file line number Diff line number Diff line change
Expand Up @@ -41,12 +41,14 @@ public NeoForgeExtension(Project project) {
}

/**
* NeoForge version number.
* NeoForge version number. You have to set either this or {@link #getNeoFormVersion()}.
*/
public abstract Property<String> getVersion();

/**
* TODO: Allow overriding the NeoForm version used specifically or use only NeoForm.
* You can set this property to a version of <a href="https://projects.neoforged.net/neoforged/neoform">NeoForm</a>
* to either override the version used in the version of NeoForge you set, or to compile against
* Vanilla artifacts that have no NeoForge code added.
*/
public abstract Property<String> getNeoFormVersion();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@
import org.gradle.api.artifacts.ExternalModuleDependency;
import org.gradle.api.artifacts.ModuleDependency;
import org.gradle.api.artifacts.dsl.RepositoryHandler;
import org.gradle.api.artifacts.repositories.MavenArtifactRepository;
import org.gradle.api.artifacts.result.ResolvedArtifactResult;
import org.gradle.api.attributes.Attribute;
import org.gradle.api.attributes.Bundling;
Expand Down Expand Up @@ -98,12 +97,21 @@ public void apply(Project project) {

var extension = project.getExtensions().create(NeoForgeExtension.NAME, NeoForgeExtension.class);
var dependencyFactory = project.getDependencyFactory();
var hasNeoForge = extension.getVersion().map(ignored -> true).orElse(false);

// When a NeoForge version is specified, we use the dependencies published by that, and otherwise
// we fall back to a potentially specified NeoForm version, which allows us to run in "Vanilla" mode.
var neoForgeModDevLibrariesDependency = extension.getVersion().map(version -> {
return dependencyFactory.create("net.neoforged:neoforge:" + version)
.capabilities(caps -> {
caps.requireCapability("net.neoforged:neoforge-dependencies");
});
});
}).orElse(extension.getNeoFormVersion().map(version -> {
return dependencyFactory.create("net.neoforged:neoform:" + version)
.capabilities(caps -> {
caps.requireCapability("net.neoforged:neoform-dependencies");
});
}));

repositories.addLast(repositories.maven(repo -> {
repo.setName("NeoForged Releases");
Expand Down Expand Up @@ -219,6 +227,14 @@ public void apply(Project project) {
config.setCanBeConsumed(false);
config.withDependencies(dependencies -> {
dependencies.addLater(minecraftClassesArtifact.map(dependencyFactory::create));
// In Vanilla-Mode, we have to add the Vanilla resources too since the legacy
// classpath (where they normally are) is ignored
dependencies.addLater(project.getProviders().zip(
createArtifacts.map(task -> project.files(task.getResourcesArtifact())),
hasNeoForge,
(resources, hasNeoForgeValue) -> !hasNeoForgeValue ? dependencyFactory.create(resources) : null
));

// Technically the Minecraft dependencies do not strictly need to be on the classpath because they are pulled from the legacy class path.
// However, we do it anyway because this matches production environments, and allows launch proxies such as DevLogin to use Minecraft's libraries.
dependencies.addLater(neoForgeModDevLibrariesDependency);
Expand Down Expand Up @@ -265,6 +281,7 @@ public void apply(Project project) {
spec.setCanBeResolved(true);
spec.setCanBeConsumed(false);
spec.shouldResolveConsistentlyWith(runtimeClasspath.get());
// NOTE: When running in vanilla mode, this configuration is simply empty
spec.withDependencies(set -> {
set.addLater(extension.getVersion().map(version -> {
return dependencyFactory.create("net.neoforged:neoforge:" + version)
Expand Down Expand Up @@ -390,19 +407,27 @@ private void configureArtifactManifestTask(CreateArtifactManifestTask task, NeoF
var configurationPrefix = "neoFormRuntimeDependencies";

Provider<ExternalModuleDependency> neoForgeDependency = extension.getVersion().map(version -> dependencyFactory.create("net.neoforged:neoforge:" + version));
Provider<ExternalModuleDependency> neoFormDependency = extension.getNeoFormVersion().map(version -> dependencyFactory.create("net.neoforged:neoform:" + version));

// Gradle prevents us from having dependencies with "incompatible attributes" in the same configuration.
// What constitutes incompatible cannot be overridden on a per-configuration basis.
var neoForgeClasses = configurations.create(configurationPrefix + "NeoForgeClasses", spec -> {
var neoForgeClassesAndData = configurations.create(configurationPrefix + "NeoForgeClasses", spec -> {
spec.setDescription("Dependencies needed for running NeoFormRuntime for the selected NeoForge/NeoForm version (NeoForge classes)");
spec.setCanBeConsumed(false);
spec.setCanBeResolved(true);
spec.withDependencies(depSpec -> depSpec.addLater(neoForgeDependency.map(dependency -> dependency.copy()
.capabilities(caps -> {
caps.requireCapability("net.neoforged:neoforge-moddev-bundle");
}))));

// This dependency is used when the NeoForm version is overridden or when we run in Vanilla-only mode
spec.withDependencies(depSpec -> depSpec.addLater(neoFormDependency.map(dependency -> dependency.copy()
.capabilities(caps -> {
caps.requireCapability("net.neoforged:neoform");
}))));
});

// This configuration is empty when running in Vanilla-mode.
var neoForgeSources = configurations.create(configurationPrefix + "NeoForgeSources", spec -> {
spec.setDescription("Dependencies needed for running NeoFormRuntime for the selected NeoForge/NeoForm version (NeoForge sources)");
spec.setCanBeConsumed(false);
Expand All @@ -424,6 +449,11 @@ private void configureArtifactManifestTask(CreateArtifactManifestTask task, NeoF
.capabilities(caps -> {
caps.requireCapability("net.neoforged:neoforge-dependencies");
}))));
// This dependency is used when the NeoForm version is overridden or when we run in Vanilla-only mode
spec.withDependencies(depSpec -> depSpec.addLater(neoFormDependency.map(dependency -> dependency.copy()
.capabilities(caps -> {
caps.requireCapability("net.neoforged:neoform-dependencies");
}))));
spec.attributes(attributes -> {
attributes.attribute(Usage.USAGE_ATTRIBUTE, project.getObjects().named(Usage.class, Usage.JAVA_API));
attributes.attribute(ATTRIBUTE_DISTRIBUTION, "client");
Expand All @@ -448,7 +478,7 @@ private void configureArtifactManifestTask(CreateArtifactManifestTask task, NeoF
});
});

for (var configuration : List.of(neoForgeClasses, neoForgeSources, compileClasspath, runtimeClasspath)) {
for (var configuration : List.of(neoForgeClassesAndData, neoForgeSources, compileClasspath, runtimeClasspath)) {
// Convert to a serializable representation for the task.
task.getNeoForgeModDevArtifacts().addAll(configuration.getIncoming().getArtifacts().getResolvedArtifacts().map(results -> {
return results.stream().map(result -> {
Expand Down Expand Up @@ -530,7 +560,7 @@ private void setupTesting(Project project,
});
});

var testLocalRuntime = configurations.create("neoForgeTestFixtures", config -> {
var testFixtures = configurations.create("neoForgeTestFixtures", config -> {
config.setDescription("Additional JUnit helpers provided by NeoForge");
config.setCanBeResolved(false);
config.setCanBeConsumed(false);
Expand All @@ -546,7 +576,7 @@ private void setupTesting(Project project,

configurations.named(JavaPlugin.TEST_RUNTIME_CLASSPATH_CONFIGURATION_NAME, files -> {
files.extendsFrom(configurations.getByName(CONFIGURATION_GENERATED_ARTIFACTS));
files.extendsFrom(testLocalRuntime);
files.extendsFrom(testFixtures);
});

var legacyClasspathConfiguration = configurations.create("neoForgeTestLibraries", spec -> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
import java.nio.file.Files;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.stream.Collectors;

Expand Down Expand Up @@ -110,13 +111,36 @@ public void prepareRun() throws IOException {
var runDir = getGameDirectory().get().getAsFile();
Files.createDirectories(runDir.toPath());

var userDevConfig = UserDevConfig.from(getNeoForgeModDevConfig().getSingleFile());
var runConfig = resolveRunType(userDevConfig);
// If no NeoForge userdev config is set, we only support Vanilla run types
UserDevRunType runConfig;
if (getNeoForgeModDevConfig().isEmpty()) {
runConfig = resolveRunType(getSimulatedUserDevConfigForVanilla());
} else {
var userDevConfig = UserDevConfig.from(getNeoForgeModDevConfig().getSingleFile());
runConfig = resolveRunType(userDevConfig);
}

writeJvmArguments(runConfig);
writeProgramArguments(runConfig);
}

private UserDevConfig getSimulatedUserDevConfigForVanilla() {
var clientArgs = List.of("--gameDir", ".", "--assetIndex", "{asset_index}", "--assetsDir", "{assets_root}", "--accessToken", "NotValid", "--version", "ModDevGradle");
var commonArgs = List.<String>of();

return new UserDevConfig("", "", "", List.of(), List.of(), Map.of(
"client", new UserDevRunType(
true, "net.minecraft.client.main.Main", clientArgs, List.of(),true, false, false, false, Map.of(), Map.of()
),
"server", new UserDevRunType(
true, "net.minecraft.server.Main", commonArgs, List.of(),false, true, false, false, Map.of(), Map.of()
),
"data", new UserDevRunType(
true, "net.minecraft.data.Main", commonArgs, List.of(),false, false, true, false, Map.of(), Map.of()
)
));
}

private void writeJvmArguments(UserDevRunType runConfig) throws IOException {
var lines = new ArrayList<String>();

Expand Down
Loading

0 comments on commit 4f5eae6

Please sign in to comment.