Skip to content
Open
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
2 changes: 2 additions & 0 deletions src/main/java/dev/railroadide/railroad/Railroad.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package dev.railroadide.railroad;

import com.github.javaparser.ParserConfiguration;
import com.github.javaparser.StaticJavaParser;
Comment on lines +3 to +4
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
import com.github.javaparser.ParserConfiguration;
import com.github.javaparser.StaticJavaParser;

import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import dev.railroadide.core.utility.ServiceLocator;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,12 +51,12 @@ public void registerDefaultProviders(@NotNull CreationStepRegistry registry, @No
new UpdateFabricModJsonStep(services.get(FilesService.class)),
new RenameMixinsStep(services.get(FilesService.class)),
new RenameClassesStep(services.get(FilesService.class)),
new UpdateGradleFilesStep(services.get(FilesService.class), services.get(HttpService.class),
new UpdateFabricGradleFilesStep(services.get(FilesService.class), services.get(HttpService.class),
services.get(TemplateEngineService.class), "dev", false),
new RunGenSourcesStep(services.get(GradleService.class)),
new InitGitStep(services.get(GitService.class))
);
} else if (type.equals(ProjectTypeRegistry.FORGE) || type.equals(ProjectTypeRegistry.NEOFORGE)) {
} else if (type.equals(ProjectTypeRegistry.FORGE)) {
registry.addAll(
new CreateDirectoriesStep(services.get(FilesService.class)),
new DownloadForgeMdkStep(
Expand All @@ -67,7 +67,26 @@ public void registerDefaultProviders(@NotNull CreationStepRegistry registry, @No
new RenamePackagesStep(services.get(FilesService.class)),
new UpdateForgeModsTomlStep(services.get(FilesService.class)),
new RenameClassesStep(services.get(FilesService.class)),
new UpdateGradleFilesStep(
new UpdateForgeGradleFilesStep(
services.get(FilesService.class), services.get(HttpService.class),
services.get(TemplateEngineService.class), "dev", true),
new CreateMixinsJsonStep(services.get(FilesService.class)),
new CreateAccessTransformerStep(services.get(FilesService.class)),
new SetupForgeGradleWrapperStep(services.get(GradleService.class)),
new InitGitStep(services.get(GitService.class))
);
} else if (type.equals(ProjectTypeRegistry.NEOFORGE)) {
registry.addAll(
new CreateDirectoriesStep(services.get(FilesService.class)),
new DownloadNeoforgeMdkStep(
services.get(HttpService.class), services.get(FilesService.class),
services.get(ZipService.class), services.get(ChecksumService.class)),
new ExtractNeoforgeMdkStep(services.get(FilesService.class), services.get(ZipService.class)),
new UpdateGradlePropertiesStep(services.get(FilesService.class)),
new RenamePackagesStep(services.get(FilesService.class)),
new UpdateForgeModsTomlStep(services.get(FilesService.class)),
new RenameClassesStep(services.get(FilesService.class)),
new UpdateForgeGradleFilesStep(
services.get(FilesService.class), services.get(HttpService.class),
services.get(TemplateEngineService.class), "dev", true),
new CreateMixinsJsonStep(services.get(FilesService.class)),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
package dev.railroadide.railroad.project.creation.step;

import dev.railroadide.core.project.ProjectContext;
import dev.railroadide.core.project.creation.CreationStep;
import dev.railroadide.core.project.creation.ProgressReporter;
import dev.railroadide.core.project.creation.service.ChecksumService;
import dev.railroadide.core.project.creation.service.FilesService;
import dev.railroadide.core.project.creation.service.HttpService;
import dev.railroadide.core.project.creation.service.ZipService;
import dev.railroadide.core.switchboard.pojo.MinecraftVersion;
import dev.railroadide.railroad.project.data.ForgeProjectKeys;
import dev.railroadide.railroad.project.data.MinecraftProjectKeys;

import java.net.URI;
import java.nio.file.Path;

public record DownloadNeoforgeMdkStep(HttpService http, FilesService files, ZipService zip,
ChecksumService checksum) implements CreationStep {
@Override
public String id() {
return "railroad:download_neoforge_mdk";
}

@Override
public String translationKey() {
return "railroad.project.creation.task.download_neoforge_mdk";
}

@Override
public void run(ProjectContext ctx, ProgressReporter reporter) throws Exception {
reporter.info("Downloading NeoForge MDK...");

String neoForgeVersion = ctx.data().getAsString(ForgeProjectKeys.FORGE_VERSION);
if (neoForgeVersion == null)
throw new IllegalStateException("NeoForge version is not set");

MinecraftVersion minecraftVersion = (MinecraftVersion) ctx.data().get(MinecraftProjectKeys.MINECRAFT_VERSION);
if (minecraftVersion == null)
throw new IllegalStateException("Minecraft version is not set");

Path projectDir = ctx.projectDir();
Path mdkZip = projectDir.resolve("neoforge-mdk.zip");

String repoBase = "https://github.com/NeoForgeMDKs/MDK-NeoForge-" + minecraftVersion.id();
Copy link

Copilot AI Nov 3, 2025

Choose a reason for hiding this comment

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

Variable 'String repoBase' is never read.

Suggested change
String repoBase = "https://github.com/NeoForgeMDKs/MDK-NeoForge-" + minecraftVersion.id();

Copilot uses AI. Check for mistakes.
String neoGradleRelease = "https://github.com/NeoForgeMDKs/MDK-" + minecraftVersion.id() + "-NeoGradle/releases/download/" + neoForgeVersion + "/";
String moddevRelease = "https://github.com/NeoForgeMDKs/MDK-" + minecraftVersion.id() + "-ModDevGradle/releases/download/" + neoForgeVersion + "/";
String fileName = "neoforged-mdk-" + neoForgeVersion + ".zip";

boolean success = false;

try {
http.download(new URI(moddevRelease + fileName), mdkZip);
success = true;
} catch (Exception ignored) {}

if (!success) {
try {
http.download(new URI(neoGradleRelease + fileName), mdkZip);
success = true;
} catch (Exception ignored) {}
}

if (!success) {
reporter.info("No release found — downloading source archive instead...");
String fallbackUrlNeoGradle = "https://github.com/NeoForgeMDKs/MDK-" + minecraftVersion.id() + "-NeoGradle/archive/refs/heads/main.zip";
String fallbackUrlModDev = "https://github.com/NeoForgeMDKs/MDK-" + minecraftVersion.id() + "-ModDevGradle/archive/refs/heads/main.zip";
try {
http.download(new URI(fallbackUrlModDev), mdkZip);
} catch (Exception e) {
http.download(new URI(fallbackUrlNeoGradle), mdkZip);
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,17 @@
import dev.railroadide.core.project.creation.ProgressReporter;
import dev.railroadide.core.project.creation.service.FilesService;
import dev.railroadide.core.project.creation.service.ZipService;
import dev.railroadide.core.switchboard.pojo.MinecraftVersion;
import dev.railroadide.railroad.project.creation.ProjectContextKeys;
import dev.railroadide.railroad.project.data.MinecraftProjectKeys;
import dev.railroadide.railroad.switchboard.SwitchboardRepositories;
import org.jetbrains.annotations.NotNull;

import java.nio.file.Path;
import java.time.ZoneOffset;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.ExecutionException;

public record ExtractForgeMdkStep(FilesService files, ZipService zip) implements CreationStep {
@Override
Expand All @@ -31,5 +40,89 @@ public void run(ProjectContext ctx, ProgressReporter reporter) throws Exception

reporter.info("Deleting Forge MDK archive...");
files.delete(archive);

MinecraftVersion requested = ctx.data().get(MinecraftProjectKeys.MINECRAFT_VERSION, MinecraftVersion.class);
if (requested == null)
throw new IllegalStateException("Minecraft version is not specified.");

reporter.info("Resolving MDK version for " + requested.id() + "...");
MinecraftVersion resolved = resolveMdkVersion(requested);
ctx.put(ProjectContextKeys.MDK_VERSION, resolved);
ctx.put(ProjectContextKeys.EXAMPLE_MOD_BRANCH, resolved.id());
}

private MinecraftVersion resolveMdkVersion(MinecraftVersion version) {
MinecraftVersion.Type type = version.getType();

// 1.21.1 -> 1.21, 1.20.2 -> 1.20, etc.
if (type != MinecraftVersion.Type.SNAPSHOT) {
if (version.id().matches("\\d+\\.\\d+"))
return version;

String releaseId = version.id().replaceAll("\\.\\d+$", "");
Optional<MinecraftVersion> release = fetchVersion(releaseId);
return release.orElse(version);
}

String id = version.id();
int dashIndex = id.indexOf('-');
if (dashIndex > 0) {
String releaseId = id.substring(0, dashIndex);
Optional<MinecraftVersion> release = fetchVersion(releaseId);
if (release.isPresent())
return release.get();
}

return findClosestRelease(version);
}

// TODO: Make sure that this will always pick a newer closest release (if it exists) instead of an older one.
private @NotNull MinecraftVersion findClosestRelease(MinecraftVersion version) {
List<MinecraftVersion> versions = fetchAllVersions();
long releaseTime = version.releaseTime().toEpochSecond(ZoneOffset.UTC);
MinecraftVersion closest = null;
for (MinecraftVersion candidate : versions) {
if (candidate.getType() != MinecraftVersion.Type.RELEASE)
continue;

if (closest == null) {
closest = candidate;
continue;
}

long epochSecond = candidate.releaseTime().toEpochSecond(ZoneOffset.UTC);
long candidateDiff = Math.abs(epochSecond - releaseTime);
long closestDiff = Math.abs(epochSecond - releaseTime);
Copy link

Copilot AI Nov 3, 2025

Choose a reason for hiding this comment

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

The calculation for closestDiff is using epochSecond instead of closest.releaseTime().toEpochSecond(ZoneOffset.UTC). This means closestDiff will always equal candidateDiff when comparing against the same release time, making the comparison logic incorrect and preventing the algorithm from finding the actual closest release.

Suggested change
long closestDiff = Math.abs(epochSecond - releaseTime);
long closestDiff = Math.abs(closest.releaseTime().toEpochSecond(ZoneOffset.UTC) - releaseTime);

Copilot uses AI. Check for mistakes.
if (candidateDiff < closestDiff) {
closest = candidate;
}
}

if (closest != null)
return closest;

throw new IllegalStateException("Forge does not support this Minecraft version");
}

private Optional<MinecraftVersion> fetchVersion(String id) {
try {
return SwitchboardRepositories.MINECRAFT.getVersionSync(id);
} catch (InterruptedException exception) {
Thread.currentThread().interrupt();
Comment on lines +110 to +111
Copy link

Copilot AI Nov 3, 2025

Choose a reason for hiding this comment

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

After re-interrupting the thread, the exception is wrapped and rethrown as an IllegalStateException. However, the interrupt status should be preserved by either propagating the InterruptedException or re-checking the interrupt status after catching it to ensure proper interrupt handling.

Copilot uses AI. Check for mistakes.
throw new IllegalStateException("Interrupted while resolving MDK version", exception);
} catch (ExecutionException exception) {
throw new IllegalStateException("Failed to resolve MDK version", exception);
}
}

private List<MinecraftVersion> fetchAllVersions() {
try {
return SwitchboardRepositories.MINECRAFT.getAllVersionsSync();
} catch (InterruptedException exception) {
Thread.currentThread().interrupt();
throw new IllegalStateException("Interrupted while resolving MDK version", exception);
} catch (ExecutionException exception) {
throw new IllegalStateException("Failed to resolve MDK version", exception);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
package dev.railroadide.railroad.project.creation.step;

import dev.railroadide.core.project.ProjectContext;
import dev.railroadide.core.project.creation.CreationStep;
import dev.railroadide.core.project.creation.ProgressReporter;
import dev.railroadide.core.project.creation.service.FilesService;
import dev.railroadide.core.project.creation.service.ZipService;
import dev.railroadide.core.switchboard.pojo.MinecraftVersion;
import dev.railroadide.railroad.project.creation.ProjectContextKeys;
import dev.railroadide.railroad.project.data.MinecraftProjectKeys;
import dev.railroadide.railroad.switchboard.SwitchboardRepositories;
import org.jetbrains.annotations.NotNull;

import java.nio.file.Files;
import java.nio.file.Path;
import java.time.ZoneOffset;
import java.util.List;
import java.util.Optional;
import java.util.Properties;
import java.util.concurrent.ExecutionException;

public record ExtractNeoforgeMdkStep(FilesService files, ZipService zip) implements CreationStep {
private static final String GRADLE_VERSION = "8.14.3";
Copy link
Member

Choose a reason for hiding this comment

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

This cannot be correct? Having a hardcoded constant for the gradle version can't be right. If this is a temporary thing, a TODO needs to be added, and mention this somewhere in the PR.


@Override
public String id() {
return "railroad:extract_neoforge_mdk";
}

@Override
public String translationKey() {
return "railroad.project.creation.task.extracting_neoforge_mdk";
}

@Override
public void run(ProjectContext ctx, ProgressReporter reporter) throws Exception {
Path projectDir = ctx.projectDir();
Path archive = projectDir.resolve("neoforge-mdk.zip");
if (!files.exists(archive))
throw new IllegalStateException("NeoForge MDK archive not found: " + archive);

reporter.info("Extracting NeoForge MDK archive...");
zip.unzip(archive, projectDir);

try (var stream = Files.list(projectDir)) {
List<Path> entries = stream
.filter(path -> !path.getFileName().toString().equals("neoforge-mdk.zip"))
.toList();

if (entries.size() == 1 && Files.isDirectory(entries.getFirst())) {
Path innerDir = entries.getFirst();
files.extractDirectoryContents(innerDir, projectDir);
files.deleteDirectory(innerDir);
}
}

reporter.info("Deleting NeoForge MDK archive...");
files.delete(archive);

reporter.info("Updating Gradle wrapper to version " + GRADLE_VERSION + "...");
updateGradleWrapperVersion(projectDir);

MinecraftVersion requested = ctx.data().get(MinecraftProjectKeys.MINECRAFT_VERSION, MinecraftVersion.class);
if (requested == null)
throw new IllegalStateException("Minecraft version is not specified.");

reporter.info("Resolving NeoForge MDK version for " + requested.id() + "...");
MinecraftVersion resolved = resolveMdkVersion(requested);
ctx.put(ProjectContextKeys.MDK_VERSION, resolved);
ctx.put(ProjectContextKeys.EXAMPLE_MOD_BRANCH, resolved.id());
}

private void updateGradleWrapperVersion(Path projectDir) throws Exception {
Path wrapperPropertiesPath = projectDir.resolve("gradle/wrapper/gradle-wrapper.properties");

if (!files.exists(wrapperPropertiesPath)) {
throw new IllegalStateException("gradle-wrapper.properties not found at: " + wrapperPropertiesPath);
}

String content = files.readString(wrapperPropertiesPath);
String newDistributionUrl = "https\\://services.gradle.org/distributions/gradle-" + GRADLE_VERSION + "-bin.zip";

String updatedContent = content.replaceAll(
"distributionUrl=https\\\\://services\\.gradle\\.org/distributions/gradle-[^\\s]+",
"distributionUrl=" + newDistributionUrl
);

files.writeString(wrapperPropertiesPath, updatedContent);
}

private MinecraftVersion resolveMdkVersion(MinecraftVersion version) {
MinecraftVersion.Type type = version.getType();

if (type != MinecraftVersion.Type.SNAPSHOT) {
if (version.id().matches("\\d+\\.\\d+"))
return version;

String releaseId = version.id().replaceAll("\\.\\d+$", "");
Optional<MinecraftVersion> release = fetchVersion(releaseId);
return release.orElse(version);
}

String id = version.id();
int dashIndex = id.indexOf('-');
if (dashIndex > 0) {
String releaseId = id.substring(0, dashIndex);
Optional<MinecraftVersion> release = fetchVersion(releaseId);
if (release.isPresent())
return release.get();
}

return findClosestRelease(version);
}

private @NotNull MinecraftVersion findClosestRelease(MinecraftVersion version) {
List<MinecraftVersion> versions = fetchAllVersions();
long releaseTime = version.releaseTime().toEpochSecond(ZoneOffset.UTC);
MinecraftVersion closest = null;
for (MinecraftVersion candidate : versions) {
if (candidate.getType() != MinecraftVersion.Type.RELEASE)
continue;

if (closest == null) {
closest = candidate;
continue;
}

long epochSecond = candidate.releaseTime().toEpochSecond(ZoneOffset.UTC);
long candidateDiff = Math.abs(epochSecond - releaseTime);
long closestDiff = Math.abs(closest.releaseTime().toEpochSecond(ZoneOffset.UTC) - releaseTime);
if (candidateDiff < closestDiff)
closest = candidate;
}

if (closest != null)
return closest;

throw new IllegalStateException("NeoForge does not support this Minecraft version");
}

private Optional<MinecraftVersion> fetchVersion(String id) {
try {
return SwitchboardRepositories.MINECRAFT.getVersionSync(id);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new IllegalStateException("Interrupted while resolving MDK version", e);
} catch (ExecutionException e) {
throw new IllegalStateException("Failed to resolve MDK version", e);
}
}

private List<MinecraftVersion> fetchAllVersions() {
try {
return SwitchboardRepositories.MINECRAFT.getAllVersionsSync();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new IllegalStateException("Interrupted while resolving MDK version", e);
} catch (ExecutionException e) {
throw new IllegalStateException("Failed to resolve MDK version", e);
}
}
}
Loading