-
-
Notifications
You must be signed in to change notification settings - Fork 17
Forge and Neoforge onboarding #99
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: dev
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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(); | ||||
|
||||
| String repoBase = "https://github.com/NeoForgeMDKs/MDK-NeoForge-" + minecraftVersion.id(); |
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -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 | ||||||
|
|
@@ -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); | ||||||
|
||||||
| long closestDiff = Math.abs(epochSecond - releaseTime); | |
| long closestDiff = Math.abs(closest.releaseTime().toEpochSecond(ZoneOffset.UTC) - releaseTime); |
Copilot
AI
Nov 3, 2025
There was a problem hiding this comment.
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.
| 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"; | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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); | ||
| } | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.