diff --git a/src/main/java/dev/railroadide/railroad/Railroad.java b/src/main/java/dev/railroadide/railroad/Railroad.java index 536b5416..a89310ce 100644 --- a/src/main/java/dev/railroadide/railroad/Railroad.java +++ b/src/main/java/dev/railroadide/railroad/Railroad.java @@ -1,5 +1,7 @@ package dev.railroadide.railroad; +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; diff --git a/src/main/java/dev/railroadide/railroad/project/creation/DefaultProjectCreationPipelineService.java b/src/main/java/dev/railroadide/railroad/project/creation/DefaultProjectCreationPipelineService.java index 58ec63fe..4efbe80a 100644 --- a/src/main/java/dev/railroadide/railroad/project/creation/DefaultProjectCreationPipelineService.java +++ b/src/main/java/dev/railroadide/railroad/project/creation/DefaultProjectCreationPipelineService.java @@ -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( @@ -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)), diff --git a/src/main/java/dev/railroadide/railroad/project/creation/step/DownloadNeoforgeMdkStep.java b/src/main/java/dev/railroadide/railroad/project/creation/step/DownloadNeoforgeMdkStep.java new file mode 100644 index 00000000..cae5e8e1 --- /dev/null +++ b/src/main/java/dev/railroadide/railroad/project/creation/step/DownloadNeoforgeMdkStep.java @@ -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 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); + } + } + } +} diff --git a/src/main/java/dev/railroadide/railroad/project/creation/step/ExtractForgeMdkStep.java b/src/main/java/dev/railroadide/railroad/project/creation/step/ExtractForgeMdkStep.java index 91dafd9f..ece8970b 100644 --- a/src/main/java/dev/railroadide/railroad/project/creation/step/ExtractForgeMdkStep.java +++ b/src/main/java/dev/railroadide/railroad/project/creation/step/ExtractForgeMdkStep.java @@ -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 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 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 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); + if (candidateDiff < closestDiff) { + closest = candidate; + } + } + + if (closest != null) + return closest; + + throw new IllegalStateException("Forge does not support this Minecraft version"); + } + + private Optional fetchVersion(String id) { + try { + return SwitchboardRepositories.MINECRAFT.getVersionSync(id); + } 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); + } + } + + private List 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); + } } } diff --git a/src/main/java/dev/railroadide/railroad/project/creation/step/ExtractNeoforgeMdkStep.java b/src/main/java/dev/railroadide/railroad/project/creation/step/ExtractNeoforgeMdkStep.java new file mode 100644 index 00000000..6d715caa --- /dev/null +++ b/src/main/java/dev/railroadide/railroad/project/creation/step/ExtractNeoforgeMdkStep.java @@ -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"; + + @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 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 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 release = fetchVersion(releaseId); + if (release.isPresent()) + return release.get(); + } + + return findClosestRelease(version); + } + + private @NotNull MinecraftVersion findClosestRelease(MinecraftVersion version) { + List 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 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 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); + } + } +} diff --git a/src/main/java/dev/railroadide/railroad/project/creation/step/RenameClassesStep.java b/src/main/java/dev/railroadide/railroad/project/creation/step/RenameClassesStep.java index 63278465..570a2c90 100644 --- a/src/main/java/dev/railroadide/railroad/project/creation/step/RenameClassesStep.java +++ b/src/main/java/dev/railroadide/railroad/project/creation/step/RenameClassesStep.java @@ -1,5 +1,6 @@ package dev.railroadide.railroad.project.creation.step; +import com.github.javaparser.ParserConfiguration; import com.github.javaparser.StaticJavaParser; import com.github.javaparser.ast.CompilationUnit; import com.github.javaparser.printer.configuration.DefaultConfigurationOption; @@ -56,6 +57,10 @@ public void run(ProjectContext ctx, ProgressReporter reporter) throws Exception compilationUnit.setPackageDeclaration(groupId + "." + modId); files.writeString(newMainClassPath, compilationUnit.toString(DEFAULT_PRINTER_CONFIGURATION)); + ParserConfiguration config = new ParserConfiguration(); + config.setLanguageLevel(ParserConfiguration.LanguageLevel.JAVA_17); + StaticJavaParser.setConfiguration(config); + // Find the example mixin Path mixinPath = rootPackagePath.resolve("mixins").resolve("ExampleMixin.java"); if (files.exists(mixinPath)) { diff --git a/src/main/java/dev/railroadide/railroad/project/creation/step/UpdateGradleFilesStep.java b/src/main/java/dev/railroadide/railroad/project/creation/step/UpdateFabricGradleFilesStep.java similarity index 97% rename from src/main/java/dev/railroadide/railroad/project/creation/step/UpdateGradleFilesStep.java rename to src/main/java/dev/railroadide/railroad/project/creation/step/UpdateFabricGradleFilesStep.java index ffbd83fd..0adfb176 100644 --- a/src/main/java/dev/railroadide/railroad/project/creation/step/UpdateGradleFilesStep.java +++ b/src/main/java/dev/railroadide/railroad/project/creation/step/UpdateFabricGradleFilesStep.java @@ -24,8 +24,8 @@ import java.util.Locale; import java.util.Map; -public record UpdateGradleFilesStep(FilesService files, HttpService http, TemplateEngineService templateEngine, - String branch, boolean includeSettingsGradle) implements CreationStep { +public record UpdateFabricGradleFilesStep(FilesService files, HttpService http, TemplateEngineService templateEngine, + String branch, boolean includeSettingsGradle) implements CreationStep { private static final String TEMPLATE_BUILD_GRADLE_URL = "https://raw.githubusercontent.com/Railroad-Team/Railroad/%s/templates/fabric/%s/template_build.gradle"; private static final String TEMPLATE_SETTINGS_GRADLE_URL = "https://raw.githubusercontent.com/Railroad-Team/Railroad/%s/templates/fabric/%s/template_settings.gradle"; diff --git a/src/main/java/dev/railroadide/railroad/project/creation/step/UpdateForgeGradleFilesStep.java b/src/main/java/dev/railroadide/railroad/project/creation/step/UpdateForgeGradleFilesStep.java new file mode 100644 index 00000000..d4ab9888 --- /dev/null +++ b/src/main/java/dev/railroadide/railroad/project/creation/step/UpdateForgeGradleFilesStep.java @@ -0,0 +1,173 @@ +package dev.railroadide.railroad.project.creation.step; + +import dev.railroadide.core.project.ProjectContext; +import dev.railroadide.core.project.ProjectData; +import dev.railroadide.core.project.ProjectType; +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.HttpService; +import dev.railroadide.core.project.creation.service.TemplateEngineService; +import dev.railroadide.core.project.minecraft.MappingChannel; +import dev.railroadide.core.switchboard.pojo.MinecraftVersion; +import dev.railroadide.railroad.project.MappingChannelRegistry; +import dev.railroadide.railroad.project.ProjectTypeRegistry; +import dev.railroadide.railroad.project.creation.ProjectContextKeys; +import dev.railroadide.railroad.project.data.FabricProjectKeys; +import dev.railroadide.railroad.project.data.ForgeProjectKeys; +import dev.railroadide.railroad.project.data.MinecraftProjectKeys; +import groovy.lang.Binding; + +import java.net.URI; +import java.nio.file.Path; +import java.util.HashMap; +import java.util.Locale; +import java.util.Map; + +public record UpdateForgeGradleFilesStep(FilesService files, HttpService http, TemplateEngineService templateEngine, + String branch, boolean includeSettingsGradle) implements CreationStep { + private static final String TEMPLATE_BUILD_GRADLE_URL = "https://raw.githubusercontent.com/Railroad-Team/Railroad/%s/templates/forge/%s/template_build.gradle"; + + private static final String TEMPLATE_SETTINGS_GRADLE_URL = "https://raw.githubusercontent.com/Railroad-Team/Railroad/%s/templates/forge/%s/template_settings.gradle"; + + @Override + public String id() { + return "railroad:update_gradle_files"; + } + + @Override + public String translationKey() { + return "railroad.project.creation.task.update_gradle_files"; + } + + @Override + public void run(ProjectContext ctx, ProgressReporter reporter) throws Exception { + updateBuildGradle(ctx, reporter); + if (includeSettingsGradle) + updateSettingsGradle(ctx, reporter); + } + + private void updateBuildGradle(ProjectContext ctx, ProgressReporter reporter) throws Exception { + reporter.info("Downloading template build.gradle..."); + + Path projectDir = ctx.projectDir(); + Path buildGradlePath = projectDir.resolve("build.gradle"); + + MinecraftVersion mdkVersion = ctx.get(ProjectContextKeys.MDK_VERSION); + if (mdkVersion == null) + throw new IllegalStateException("MDK version not set in project context"); + + String templateBuildGradleUrl = TEMPLATE_BUILD_GRADLE_URL.formatted(branch, mdkVersion.id().substring("1.".length())); + if (http.isNotFound(new URI(templateBuildGradleUrl))) { + MinecraftVersion minecraftVersion = ctx.data().get(MinecraftProjectKeys.MINECRAFT_VERSION, MinecraftVersion.class); + if (minecraftVersion == null) + throw new IllegalStateException("Minecraft version not set in project context"); + + templateBuildGradleUrl = TEMPLATE_BUILD_GRADLE_URL.formatted(branch, minecraftVersion.id().substring("1.".length())); + if (http.isNotFound(new URI(templateBuildGradleUrl))) + throw new IllegalStateException("Template build.gradle not found for version " + mdkVersion.id() + " or " + minecraftVersion.id()); + } + + Path templateBuildGradlePath = buildGradlePath.resolveSibling("template_build.gradle"); + http.download(new URI(templateBuildGradleUrl), templateBuildGradlePath); + + reporter.info("Updating build.gradle..."); + String templateContent = files.readString(templateBuildGradlePath); + if (!templateContent.startsWith("// fileName: ")) + throw new IllegalStateException("Invalid template build.gradle file: missing fileName metadata"); + + updateContent(ctx, projectDir, buildGradlePath, templateBuildGradlePath, templateContent); + } + + private void updateSettingsGradle(ProjectContext ctx, ProgressReporter reporter) throws Exception { + reporter.info("Downloading template settings.gradle..."); + + Path projectDir = ctx.projectDir(); + Path settingsGradlePath = projectDir.resolve("settings.gradle"); + + MinecraftVersion mdkVersion = ctx.get(ProjectContextKeys.MDK_VERSION); + if (mdkVersion == null) + throw new IllegalStateException("MDK version not set in project context"); + + String templateSettingsGradleUrl = TEMPLATE_SETTINGS_GRADLE_URL.formatted(branch, mdkVersion.id().substring("1.".length())); + if (http.isNotFound(new URI(templateSettingsGradleUrl))) { + MinecraftVersion minecraftVersion = ctx.data().get(MinecraftProjectKeys.MINECRAFT_VERSION, MinecraftVersion.class); + if (minecraftVersion == null) + throw new IllegalStateException("Minecraft version not set in project context"); + + templateSettingsGradleUrl = TEMPLATE_SETTINGS_GRADLE_URL.formatted(branch, minecraftVersion.id().substring("1.".length())); + if (http.isNotFound(new URI(templateSettingsGradleUrl))) + throw new IllegalStateException("Template settings.gradle not found for version " + mdkVersion.id() + " or " + minecraftVersion.id()); + } + + Path templateSettingsGradlePath = settingsGradlePath.resolveSibling("template_settings.gradle"); + http.download(new URI(templateSettingsGradleUrl), templateSettingsGradlePath); + + reporter.info("Updating settings.gradle..."); + String templateContent = files.readString(templateSettingsGradlePath); + if (!templateContent.startsWith("// fileName: ")) + throw new IllegalStateException("Invalid template settings.gradle file: missing fileName metadata"); + + updateContent(ctx, projectDir, settingsGradlePath, templateSettingsGradlePath, templateContent); + } + + private void updateContent(ProjectContext ctx, Path projectDir, Path settingsGradlePath, Path templateSettingsGradlePath, String templateContent) throws Exception { + Map args = createGradleBindings(ctx.data()); + var binding = new Binding(args); + binding.setVariable("defaultName", projectDir.relativize(settingsGradlePath.toAbsolutePath()).toString()); + + String updatedContent = templateEngine.apply(templateContent, args); + files.writeString(settingsGradlePath, updatedContent); + files.delete(templateSettingsGradlePath); + } + + private static Map createGradleBindings(ProjectData data) { + ProjectType projectType = data.get(ProjectData.DefaultKeys.TYPE, ProjectType.class); + if (projectType == null) + throw new IllegalStateException("Project type not set in project data"); + + MappingChannel defaultChannel = getDefaultMappingChannel(projectType); + + final Map args = new HashMap<>(); + args.put("mappings", Map.of( + "channel", data.getOrDefault(MinecraftProjectKeys.MAPPING_CHANNEL, defaultChannel, MappingChannel.class).id().toLowerCase(Locale.ROOT), + "version", data.getAsString(MinecraftProjectKeys.MAPPING_VERSION) + )); + + Map props = new HashMap<>(); + if (projectType == ProjectTypeRegistry.FABRIC) { + props.putAll(Map.of( + "splitSourceSets", data.getAsBoolean(FabricProjectKeys.SPLIT_SOURCES), + "includeFabricApi", data.contains(FabricProjectKeys.FABRIC_API_VERSION), + "useAccessWidener", data.getAsBoolean(FabricProjectKeys.USE_ACCESS_WIDENER), + "accessWidenerPath", data.contains(FabricProjectKeys.ACCESS_WIDENER_PATH) ? + data.getAsString(FabricProjectKeys.ACCESS_WIDENER_PATH) : + data.getAsString(MinecraftProjectKeys.MOD_ID) + ".accesswidener", + "modId", data.getAsString(MinecraftProjectKeys.MOD_ID) + )); + } else if (projectType == ProjectTypeRegistry.FORGE || projectType == ProjectTypeRegistry.NEOFORGE) { + props.putAll(Map.of( + "useMixins", data.getAsBoolean(ForgeProjectKeys.USE_MIXINS), + "useAccessTransformer", data.getAsBoolean(ForgeProjectKeys.USE_ACCESS_TRANSFORMER), + "genRunFolders", data.getAsBoolean(ForgeProjectKeys.GEN_RUN_FOLDERS) + )); + } else { + throw new IllegalStateException("Unsupported project type: " + projectType); + } + + args.put("props", props); + return args; + } + + public static MappingChannel getDefaultMappingChannel(ProjectType projectType) { + if (projectType.equals(ProjectTypeRegistry.FABRIC)) { + return MappingChannelRegistry.YARN; + } else if (projectType.equals(ProjectTypeRegistry.FORGE)) { + return MappingChannelRegistry.MOJMAP; + } else if (projectType.equals(ProjectTypeRegistry.NEOFORGE)) { + return MappingChannelRegistry.PARCHMENT; + } + + throw new IllegalStateException("Unsupported project type: " + projectType); + } +} diff --git a/src/main/java/dev/railroadide/railroad/project/creation/step/UpdateForgeModsTomlStep.java b/src/main/java/dev/railroadide/railroad/project/creation/step/UpdateForgeModsTomlStep.java index ffc182db..ba15f3ed 100644 --- a/src/main/java/dev/railroadide/railroad/project/creation/step/UpdateForgeModsTomlStep.java +++ b/src/main/java/dev/railroadide/railroad/project/creation/step/UpdateForgeModsTomlStep.java @@ -31,8 +31,21 @@ public void run(ProjectContext ctx, ProgressReporter reporter) throws Exception reporter.info("Updating mods.toml..."); Path modsTomlPath = ctx.projectDir().resolve("src/main/resources/META-INF/mods.toml"); - if (!files.exists(modsTomlPath)) + if (!files.exists(modsTomlPath)) { + modsTomlPath = ctx.projectDir().resolve("src/main/resources/META-INF/neoforge.mods.toml"); + } + + if (!files.exists(modsTomlPath)) { + modsTomlPath = ctx.projectDir().resolve("src/main/templates/META-INF/mods.toml"); + } + + if (!files.exists(modsTomlPath)) { + modsTomlPath = ctx.projectDir().resolve("src/main/templates/META-INF/neoforge.mods.toml"); + } + + if (!files.exists(modsTomlPath)) { throw new IllegalStateException("mods.toml not found at " + modsTomlPath); + } boolean hasIssues = ctx.data().contains(ProjectData.DefaultKeys.ISSUES_URL); boolean hasUpdateJson = ctx.data().contains(ForgeProjectKeys.UPDATE_JSON_URL); diff --git a/src/main/java/dev/railroadide/railroad/project/creation/step/UpdateGradlePropertiesStep.java b/src/main/java/dev/railroadide/railroad/project/creation/step/UpdateGradlePropertiesStep.java index 1bddc5b7..5a9108fd 100644 --- a/src/main/java/dev/railroadide/railroad/project/creation/step/UpdateGradlePropertiesStep.java +++ b/src/main/java/dev/railroadide/railroad/project/creation/step/UpdateGradlePropertiesStep.java @@ -48,7 +48,8 @@ public void run(ProjectContext ctx, ProgressReporter reporter) throws IOExceptio MappingChannel mappingChannel = ctx.data().get(MinecraftProjectKeys.MAPPING_CHANNEL, MappingChannel.class); if (mappingChannel == null) - mappingChannel = UpdateGradleFilesStep.getDefaultMappingChannel(projectType); + mappingChannel = UpdateFabricGradleFilesStep.getDefaultMappingChannel(projectType); + String channelId = mappingChannel.id().toLowerCase(Locale.ROOT); if (channelId.equals("mojmap")) channelId = "official"; @@ -64,11 +65,12 @@ public void run(ProjectContext ctx, ProgressReporter reporter) throws IOExceptio String modId = ctx.data().getAsString(MinecraftProjectKeys.MOD_ID); String modName = ctx.data().getAsString(MinecraftProjectKeys.MOD_NAME); License license = ctx.data().getOrDefault(ProjectData.DefaultKeys.LICENSE, LicenseRegistry.LGPL, License.class); - String licenseStr = license == LicenseRegistry.CUSTOM ? ctx.data().getAsString(ProjectData.DefaultKeys.LICENSE_CUSTOM) : license.getSpdxId(); + String licenseStr = license == LicenseRegistry.CUSTOM + ? ctx.data().getAsString(ProjectData.DefaultKeys.LICENSE_CUSTOM) + : license.getSpdxId(); String version = ctx.data().getAsString(MavenProjectKeys.VERSION); String groupId = ctx.data().getAsString(MavenProjectKeys.GROUP_ID); - String authors = ctx.data().getAsString(ProjectData.DefaultKeys.AUTHOR, ""); String description = ctx.data().getAsString(ProjectData.DefaultKeys.DESCRIPTION, ""); @@ -82,6 +84,7 @@ public void run(ProjectContext ctx, ProgressReporter reporter) throws IOExceptio files.updateKeyPairInPropertiesFile(propsFile, "minecraft_version", minecraftVersion.id()); files.updateKeyPairInPropertiesFile(propsFile, "loader_version", loaderVersion.version()); files.updateKeyPairInPropertiesFile(propsFile, "fabric_version", fabricApiVersion); + if (mappingChannel == MappingChannelRegistry.YARN) { files.updateKeyPairInPropertiesFile(propsFile, "yarn_mappings", mappingVersion); } else if (mappingChannel == MappingChannelRegistry.PARCHMENT) { @@ -90,20 +93,39 @@ public void run(ProjectContext ctx, ProgressReporter reporter) throws IOExceptio files.updateKeyPairInPropertiesFile(propsFile, "mod_version", version); files.updateKeyPairInPropertiesFile(propsFile, "maven_group", groupId); - files.updateKeyPairInPropertiesFile(propsFile, "archives_base_name", ctx.data().getAsString(MavenProjectKeys.ARTIFACT_ID, modId)); - } else if (projectType.equals(ProjectTypeRegistry.FORGE) || projectType.equals(ProjectTypeRegistry.NEOFORGE)) {// TODO: Confirm for neoforge + files.updateKeyPairInPropertiesFile(propsFile, "archives_base_name", + ctx.data().getAsString(MavenProjectKeys.ARTIFACT_ID, modId)); + + } else if (projectType.equals(ProjectTypeRegistry.FORGE)) { + // Forge uses separate mapping_channel and mapping_version properties files.updateKeyPairInPropertiesFile(propsFile, "mapping_channel", channelId); files.updateKeyPairInPropertiesFile(propsFile, "mapping_version", mappingVersion); files.updateKeyPairInPropertiesFile(propsFile, "mod_id", modId); files.updateKeyPairInPropertiesFile(propsFile, "mod_name", modName); files.updateKeyPairInPropertiesFile(propsFile, "mod_license", licenseStr); - files.updateKeyPairInPropertiesFile(propsFile, "mod_version", version); files.updateKeyPairInPropertiesFile(propsFile, "mod_group_id", groupId); + files.updateKeyPairInPropertiesFile(propsFile, "mod_authors", authors); + files.updateKeyPairInPropertiesFile(propsFile, "mod_description", "'''" + description + "'''"); + + } else if (projectType.equals(ProjectTypeRegistry.NEOFORGE)) { + String mappingsValue = channelId + "_" + mappingVersion; + try { + files.updateKeyPairInPropertiesFile(propsFile, "mappings", mappingsValue); + } catch (Exception e) { + reporter.info("No mappings property found in gradle.properties (this is normal for NeoForge)"); + } + + files.updateKeyPairInPropertiesFile(propsFile, "mod_id", modId); + files.updateKeyPairInPropertiesFile(propsFile, "mod_name", modName); + files.updateKeyPairInPropertiesFile(propsFile, "mod_license", licenseStr); + files.updateKeyPairInPropertiesFile(propsFile, "mod_version", version); + files.updateKeyPairInPropertiesFile(propsFile, "mod_group_id", groupId); files.updateKeyPairInPropertiesFile(propsFile, "mod_authors", authors); files.updateKeyPairInPropertiesFile(propsFile, "mod_description", "'''" + description + "'''"); + } else { throw new IllegalStateException("Unsupported project type: " + projectType.getName()); } diff --git a/src/main/java/dev/railroadide/railroad/project/onboarding/flow/OnboardingFlow.java b/src/main/java/dev/railroadide/railroad/project/onboarding/flow/OnboardingFlow.java index ea32c77c..7957d704 100644 --- a/src/main/java/dev/railroadide/railroad/project/onboarding/flow/OnboardingFlow.java +++ b/src/main/java/dev/railroadide/railroad/project/onboarding/flow/OnboardingFlow.java @@ -1,11 +1,13 @@ package dev.railroadide.railroad.project.onboarding.flow; +import dev.railroadide.railroad.project.onboarding.OnboardingContext; import dev.railroadide.railroad.project.onboarding.step.OnboardingStep; import lombok.Getter; -import java.util.List; -import java.util.Map; +import java.util.*; +import java.util.function.Predicate; import java.util.function.Supplier; +import java.util.stream.Collectors; public class OnboardingFlow { private final Map> stepLookup; @@ -31,4 +33,50 @@ public Supplier lookup(String id) { public int getTotalSteps() { return stepLookup.size(); } + + public static class OnboardingFlowBuilder { + private final LinkedHashMap> stepLookup = new LinkedHashMap<>(); + private final List transitions = new ArrayList<>(); + private String firstStepId; + + public OnboardingFlowBuilder addStep(String id, Supplier step) { + stepLookup.put(id, step); + if (firstStepId == null) firstStepId = id; + return this; + } + + public OnboardingFlowBuilder firstStep(String id) { + this.firstStepId = id; + return this; + } + + public OnboardingFlowBuilder addConditionalTransition(String from, String to, Predicate condition) { + transitions.add(new OnboardingTransition(from, to, condition)); + return this; + } + + public List getTransitionsTo(String stepId) { + return transitions.stream().filter(t -> t.getToStepId().equals(stepId)).collect(Collectors.toList()); + } + + public List getTransitionsFrom(String stepId) { + return transitions.stream().filter(t -> t.getFromStepId().equals(stepId)).collect(Collectors.toList()); + } + + public void removeTransition(OnboardingTransition transition) { + transitions.remove(transition); + } + + public OnboardingFlow build() { + List keys = new ArrayList<>(stepLookup.keySet()); + for (int i = 0; i < keys.size() - 1; i++) { + String from = keys.get(i); + String to = keys.get(i + 1); + if (transitions.stream().noneMatch(t -> t.getFromStepId().equals(from) && t.getToStepId().equals(to))) { + transitions.add(new OnboardingTransition(from, to, null)); + } + } + return new OnboardingFlow(stepLookup, transitions, firstStepId); + } + } } diff --git a/src/main/java/dev/railroadide/railroad/project/onboarding/flow/OnboardingFlowBuilder.java b/src/main/java/dev/railroadide/railroad/project/onboarding/flow/OnboardingFlowBuilder.java deleted file mode 100644 index 7b96ac65..00000000 --- a/src/main/java/dev/railroadide/railroad/project/onboarding/flow/OnboardingFlowBuilder.java +++ /dev/null @@ -1,61 +0,0 @@ -package dev.railroadide.railroad.project.onboarding.flow; - -import dev.railroadide.railroad.project.onboarding.OnboardingContext; -import dev.railroadide.railroad.project.onboarding.step.OnboardingStep; - -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.function.Predicate; -import java.util.function.Supplier; -import java.util.stream.Collectors; - -public class OnboardingFlowBuilder { - private final Map> stepLookup = new HashMap<>(); - private final List transitions = new ArrayList<>(); - private String firstStepId; - - public OnboardingFlowBuilder addStep(String id, Supplier step) { - stepLookup.put(id, step); - if (firstStepId == null) { - firstStepId = id; - } - return this; - } - - public OnboardingFlowBuilder firstStep(String id) { - this.firstStepId = id; - return this; - } - - public OnboardingFlowBuilder addTransition(String from, String to) { - transitions.add(new OnboardingTransition(from, to, null)); - return this; - } - - public OnboardingFlowBuilder addConditionalTransition(String from, String to, Predicate condition) { - transitions.add(new OnboardingTransition(from, to, condition)); - return this; - } - - public List getTransitionsTo(String stepId) { - return transitions.stream() - .filter(t -> t.getToStepId().equals(stepId)) - .collect(Collectors.toList()); - } - - public List getTransitionsFrom(String stepId) { - return transitions.stream() - .filter(t -> t.getFromStepId().equals(stepId)) - .collect(Collectors.toList()); - } - - public void removeTransition(OnboardingTransition transition) { - transitions.remove(transition); - } - - public OnboardingFlow build() { - return new OnboardingFlow(stepLookup, transitions, firstStepId); - } -} diff --git a/src/main/java/dev/railroadide/railroad/project/onboarding/impl/FabricProjectOnboarding.java b/src/main/java/dev/railroadide/railroad/project/onboarding/impl/FabricProjectOnboarding.java index 6c6376b0..8b249dcf 100644 --- a/src/main/java/dev/railroadide/railroad/project/onboarding/impl/FabricProjectOnboarding.java +++ b/src/main/java/dev/railroadide/railroad/project/onboarding/impl/FabricProjectOnboarding.java @@ -52,7 +52,7 @@ import java.util.function.Consumer; import java.util.function.Function; -public class FabricProjectOnboarding { +public class FabricProjectOnboarding extends Onboarding { private final ExecutorService executor = Executors.newFixedThreadPool(4); public void start(Scene scene) { @@ -71,18 +71,6 @@ public void start(Scene scene) { .addStep("split_sources", this::createSplitSourcesStep) .addStep("optional_details", this::createOptionalDetailsStep) .firstStep("project_details") - .addTransition("project_details", "maven_coordinates") - .addTransition("maven_coordinates", "minecraft_version") - .addTransition("minecraft_version", "mapping_channel") - .addTransition("mapping_channel", "mapping_version") - .addTransition("mapping_version", "fabric_loader") - .addTransition("fabric_loader", "fabric_api") - .addTransition("fabric_api", "mod_details") - .addTransition("mod_details", "license") - .addTransition("license", "git") - .addTransition("git", "access_widener") - .addTransition("access_widener", "split_sources") - .addTransition("split_sources", "optional_details") .build(); var process = OnboardingProcess.createBasic( @@ -94,7 +82,7 @@ public void start(Scene scene) { process.run(scene); } - private void onFinish(OnboardingContext ctx, Scene scene) { + protected void onFinish(OnboardingContext ctx, Scene scene) { this.executor.shutdown(); var data = new ProjectData(); @@ -149,114 +137,6 @@ private void onFinish(OnboardingContext ctx, Scene scene) { scene.setRoot(creationPane); } - private OnboardingStep createProjectDetailsStep() { - return OnboardingFormStep.builder() - .id("project_details") - .title("railroad.project.creation.project_details.title") - .description("railroad.project.creation.project_details.description") - .appendSection("railroad.project.creation.section.project", - described( - FormComponent.textField(ProjectData.DefaultKeys.NAME, "railroad.project.creation.name") - .required() - .promptText("railroad.project.creation.name.prompt") - .validator(ProjectValidators::validateProjectName), - "railroad.project.creation.name.info"), - described( - FormComponent.directoryChooser(ProjectData.DefaultKeys.PATH, "railroad.project.creation.location") - .required() - .defaultPath(System.getProperty("user.home")) - .validator(ProjectValidators::validatePath), - value -> { - if (value == null) - return null; - - String text = value.toString(); - return text.isBlank() ? null : Path.of(text); - }, - value -> { - if (value == null) - return null; - - return value instanceof Path path ? path.toAbsolutePath().toString() : value.toString(); - }, - "railroad.project.creation.location.info")) - .build(); - } - - private OnboardingStep createMavenCoordinatesStep() { - StringProperty artifactId = new SimpleStringProperty(); - String configuredGroupId = SettingsHandler.getValue(Settings.DEFAULT_PROJECT_GROUP_ID); - String configuredVersion = SettingsHandler.getValue(Settings.DEFAULT_PROJECT_VERSION); - String defaultGroupId = isNullOrBlank(configuredGroupId) ? "" : configuredGroupId; - String defaultVersion = isNullOrBlank(configuredVersion) ? "1.0.0" : configuredVersion; - - return OnboardingFormStep.builder() - .id("maven_coordinates") - .title("railroad.project.creation.maven_coordinates.title") - .description("railroad.project.creation.maven_coordinates.description") - .appendSection("railroad.project.creation.section.maven_coordinates", - described( - FormComponent.textField(MavenProjectKeys.GROUP_ID, "railroad.project.creation.group_id") - .required() - .promptText("railroad.project.creation.group_id.prompt") - .text(() -> defaultGroupId) - .validator(ProjectValidators::validateGroupId), - "railroad.project.creation.group_id.info"), - described( - FormComponent.textField(MavenProjectKeys.ARTIFACT_ID, "railroad.project.creation.artifact_id") - .required() - .promptText("railroad.project.creation.artifact_id.prompt") - .text(artifactId::get) - .validator(ProjectValidators::validateArtifactId), - "railroad.project.creation.artifact_id.info"), - described( - FormComponent.textField(MavenProjectKeys.VERSION, "railroad.project.creation.version") - .required() - .promptText("railroad.project.creation.version.prompt") - .text(() -> defaultVersion) - .validator(ProjectValidators::validateVersion), - "railroad.project.creation.version.info")) - .onEnter(ctx -> { - String projectName = ctx.get(ProjectData.DefaultKeys.NAME); - if (projectName != null) { - String defaultArtifactId = ProjectValidators.projectNameToArtifactId(projectName); - if (isNullOrBlank(artifactId.get())) { - artifactId.set(defaultArtifactId); - } - } - }) - .build(); - } - - private OnboardingStep createMinecraftVersionStep() { - ObservableList availableVersions = FXCollections.observableArrayList(); - var nextInvalidationTime = new AtomicLong(0L); - - return OnboardingFormStep.builder() - .id("minecraft_version") - .title("railroad.project.creation.minecraft_version.title") - .description("railroad.project.creation.minecraft_version.description") - .appendSection("railroad.project.creation.section.minecraft_version", - described( - FormComponent.comboBox(MinecraftProjectKeys.MINECRAFT_VERSION, "railroad.project.creation.minecraft_version", MinecraftVersion.class) - .items(() -> availableVersions) - .defaultValue(() -> MinecraftVersion.determineDefaultMinecraftVersion(availableVersions)) - .keyFunction(MinecraftVersion::id) - .valueOfFunction(FabricProjectOnboarding::getMinecraftVersion) - .required() - .translate(false), - "railroad.project.creation.minecraft_version.info")) - .onEnter(ctx -> { - if (availableVersions.isEmpty() || System.currentTimeMillis() > nextInvalidationTime.get()) { - availableVersions.clear(); - availableVersions.addAll(getMinecraftVersions()); - nextInvalidationTime.set(System.currentTimeMillis() + TimeUnit.MINUTES.toMillis(5)); - ctx.markForRefresh(MinecraftProjectKeys.MINECRAFT_VERSION); - } - }) - .build(); - } - private OnboardingStep createMappingChannelStep() { ObservableList availableChannels = FXCollections.observableArrayList(); return OnboardingFormStep.builder() @@ -397,148 +277,6 @@ private OnboardingStep createFabricApiStep() { .build(); } - private OnboardingStep createModDetailsStep() { - StringProperty modIdProperty = new SimpleStringProperty(); - StringProperty modNameProperty = new SimpleStringProperty(); - StringProperty mainClassProperty = new SimpleStringProperty(); - - ObjectProperty modIdField = new SimpleObjectProperty<>(); - ObjectProperty modNameField = new SimpleObjectProperty<>(); - ObjectProperty mainClassField = new SimpleObjectProperty<>(); - - bindTextField(modIdProperty, modIdField); - bindTextField(modNameProperty, modNameField); - bindTextField(mainClassProperty, mainClassField); - - return OnboardingFormStep.builder() - .id("mod_details") - .title("railroad.project.creation.mod_details.title") - .description("railroad.project.creation.mod_details.description") - .appendSection("railroad.project.creation.section.mod_details", - described( - FormComponent.textField(MinecraftProjectKeys.MOD_ID, "railroad.project.creation.mod_id") - .required() - .promptText("railroad.project.creation.mod_id.prompt") - .text(modIdProperty::get) - .bindTextFieldTo(modIdField) - .validator(ProjectValidators::validateModId), - "railroad.project.creation.mod_id.info"), - described( - FormComponent.textField(MinecraftProjectKeys.MOD_NAME, "railroad.project.creation.mod_name") - .required() - .promptText("railroad.project.creation.mod_name.prompt") - .text(modNameProperty::get) - .bindTextFieldTo(modNameField) - .validator(ProjectValidators::validateModName), - "railroad.project.creation.mod_name.info"), - described( - FormComponent.textField(MinecraftProjectKeys.MAIN_CLASS, "railroad.project.creation.main_class") - .required() - .promptText("railroad.project.creation.main_class.prompt") - .text(mainClassProperty::get) - .bindTextFieldTo(mainClassField) - .validator(ProjectValidators::validateMainClass), - "railroad.project.creation.main_class.info")) - .onEnter(ctx -> { - String projectName = ctx.get(ProjectData.DefaultKeys.NAME); - - if (!isNullOrBlank(projectName)) { - modIdProperty.set(ProjectValidators.projectNameToModId(projectName)); - } - - if (!isNullOrBlank(projectName)) { - modNameProperty.set(projectName); - } - - if (!isNullOrBlank(projectName)) { - String mainClassName = ProjectValidators.projectNameToMainClass(projectName); - mainClassProperty.set(isNullOrBlank(mainClassName) ? "" : mainClassName); - } - }) - .build(); - } - - private OnboardingStep createLicenseStep() { - ObservableList availableLicenses = FXCollections.observableArrayList(); - ObjectProperty> licenseComboBox = new SimpleObjectProperty<>(); - BooleanProperty showCustomLicense = new SimpleBooleanProperty(false); - ChangeListener licenseSelectionListener = (observable, oldValue, newValue) -> - showCustomLicense.set(newValue == LicenseRegistry.CUSTOM); - - licenseComboBox.addListener((observable, oldValue, newValue) -> { - if (oldValue != null) { - oldValue.valueProperty().removeListener(licenseSelectionListener); - } - - if (newValue != null) { - showCustomLicense.set(newValue.getValue() == LicenseRegistry.CUSTOM); - newValue.valueProperty().addListener(licenseSelectionListener); - } else { - showCustomLicense.set(false); - } - }); - - BooleanBinding customLicenseVisible = Bindings.createBooleanBinding(showCustomLicense::get, showCustomLicense); - - return OnboardingFormStep.builder() - .id("license") - .title("railroad.project.creation.license.title") - .description("railroad.project.creation.license.description") - .appendSection("railroad.project.creation.section.license", - described( - FormComponent.comboBox(ProjectData.DefaultKeys.LICENSE, "railroad.project.creation.license", License.class) - .required() - .bindComboBoxTo(licenseComboBox) - .keyFunction(License::getSpdxId) - .valueOfFunction(License::fromSpdxId) - .defaultDisplayNameFunction(License::getName) - .translate(false) - .items(() -> availableLicenses) - .defaultValue(() -> { - if (availableLicenses.contains(LicenseRegistry.LGPL)) - return LicenseRegistry.LGPL; - - if (!availableLicenses.isEmpty()) - return availableLicenses.getFirst(); - - return null; - }), - "railroad.project.creation.license.info"), - described( - FormComponent.textField(ProjectData.DefaultKeys.LICENSE_CUSTOM, "railroad.project.creation.license.custom") - .visible(customLicenseVisible) - .promptText("railroad.project.creation.license.custom.prompt") - .validator(ProjectValidators::validateCustomLicense), - "railroad.project.creation.license.custom.info")) - .onEnter(ctx -> { - List newValues = License.REGISTRY.values() - .stream() - .sorted(Comparator.comparing(License::getName)) - .toList(); - - if (availableLicenses.size() != newValues.size() || !ListUtils.isEqualList(availableLicenses, newValues)) { - availableLicenses.clear(); - availableLicenses.addAll(newValues); - ctx.markForRefresh(ProjectData.DefaultKeys.LICENSE); - } - }) - .build(); - } - - private OnboardingStep createGitStep() { - // TODO: Provide options for GitHub, GitLab, Bitbucket initialization (with private/public options) - return OnboardingFormStep.builder() - .id("git") - .title("railroad.project.creation.git.title") - .description("railroad.project.creation.git.description") - .appendSection("railroad.project.creation.section.git", - described( - FormComponent.checkBox(ProjectData.DefaultKeys.INIT_GIT, "railroad.project.creation.init_git") - .selected(true), - "railroad.project.creation.init_git.info")) - .build(); - } - private OnboardingStep createAccessWidenerStep() { ObjectProperty useAccessWidenerCheckBox = new SimpleObjectProperty<>(); BooleanProperty accessWidenerEnabled = new SimpleBooleanProperty(true); @@ -610,90 +348,8 @@ private OnboardingStep createSplitSourcesStep() { .build(); } - private OnboardingStep createOptionalDetailsStep() { - String configuredAuthor = SettingsHandler.getValue(Settings.DEFAULT_PROJECT_AUTHOR); - String defaultAuthor = !isNullOrBlank(configuredAuthor) - ? configuredAuthor - : Optional.ofNullable(System.getProperty("user.name")) - .filter(name -> !isNullOrBlank(name)) - .orElse(""); - - return OnboardingFormStep.builder() - .id("optional_details") - .title("railroad.project.creation.optional_details.title") - .description("railroad.project.creation.optional_details.description") - .appendSection("railroad.project.creation.section.optional_details", - described( - FormComponent.textField(ProjectData.DefaultKeys.AUTHOR, "railroad.project.creation.author") - .text(() -> defaultAuthor) - .promptText("railroad.project.creation.author.prompt") - .validator(ProjectValidators::validateAuthor), - "railroad.project.creation.author.info"), - described( - FormComponent.textArea(ProjectData.DefaultKeys.DESCRIPTION, "railroad.project.creation.description") - .promptText("railroad.project.creation.description.prompt") - .validator(ProjectValidators::validateDescription), - "railroad.project.creation.description.info"), - described( - FormComponent.textField(ProjectData.DefaultKeys.ISSUES_URL, "railroad.project.creation.issues_url") - .promptText("railroad.project.creation.issues_url.prompt") - .validator(ProjectValidators::validateIssues), - "railroad.project.creation.issues_url.info"), - described( - FormComponent.textField(ProjectData.DefaultKeys.HOMEPAGE_URL, "railroad.project.creation.homepage_url") - .promptText("railroad.project.creation.homepage_url.prompt") - .validator(textField -> ProjectValidators.validateGenericUrl(textField, "homepage")), - "railroad.project.creation.homepage_url.info"), - described( - FormComponent.textField(ProjectData.DefaultKeys.SOURCES_URL, "railroad.project.creation.sources_url") - .promptText("railroad.project.creation.sources_url.prompt") - .validator(textField -> ProjectValidators.validateGenericUrl(textField, "sources")), - "railroad.project.creation.sources_url.info")) - .build(); - } - - private static OnboardingFormStep.ComponentSpec described(FormComponentBuilder builder, String descriptionKey) { - return OnboardingFormStep.component(builder, createDescriptionCustomizer(descriptionKey)); - } - - private static OnboardingFormStep.ComponentSpec described(FormComponentBuilder builder, Function transformer, Function reverseTransformer, String descriptionKey) { - return OnboardingFormStep.component(builder, builder != null ? builder.dataKey() : null, transformer, reverseTransformer, createDescriptionCustomizer(descriptionKey)); - } - - private static Consumer> createDescriptionCustomizer(String descriptionKey) { - if (isNullOrBlank(descriptionKey)) - return null; - - return component -> attachDescription(component, descriptionKey); - } - - private static void attachDescription(FormComponent component, String descriptionKey) { - if (component == null || isNullOrBlank(descriptionKey)) - return; - - Consumer applyToNode = node -> { - if (node instanceof InformativeLabeledHBox informative) { - boolean exists = informative.getInformationLabels().stream() - .anyMatch(label -> descriptionKey.equals(label.getKey())); - if (!exists) { - informative.addInformationLabel(descriptionKey); - } - } - }; - - Node currentNode = component.componentProperty().get(); - if (currentNode != null) { - applyToNode.accept(currentNode); - } - - component.componentProperty().addListener((observable, oldValue, newValue) -> { - if (newValue != null) { - applyToNode.accept(newValue); - } - }); - } - - private static @NotNull List getMinecraftVersions() { + @Override + protected @NotNull List getMinecraftVersions() { try { return SwitchboardRepositories.FABRIC_API.getAllVersionsSync().stream() .map(FabricApiVersionRepository::fapiToMinecraftVersion) @@ -709,44 +365,4 @@ private static void attachDescription(FormComponent component, Strin return Collections.emptyList(); } } - - private static MinecraftVersion getMinecraftVersion(String string) { - try { - return SwitchboardRepositories.MINECRAFT.getVersionSync(string).orElse(null); - } catch (ExecutionException | InterruptedException exception) { - Railroad.LOGGER.error("Failed to fetch Minecraft version {}", string, exception); - return null; - } - } - - private static boolean isNullOrBlank(String value) { - return value == null || value.isBlank(); - } - - private static void bindTextField(StringProperty valueProperty, ObjectProperty fieldProperty) { - Objects.requireNonNull(valueProperty, "valueProperty"); - Objects.requireNonNull(fieldProperty, "fieldProperty"); - - valueProperty.addListener((obs, oldValue, newValue) -> { - TextField field = fieldProperty.get(); - if (field != null && !Objects.equals(field.getText(), newValue)) { - field.setText(newValue); - } - }); - - fieldProperty.addListener((obs, oldField, newField) -> { - if (newField == null) - return; - - if (!Objects.equals(newField.getText(), valueProperty.get())) { - newField.setText(valueProperty.get()); - } - - newField.textProperty().addListener((textObs, oldText, newText) -> { - if (!Objects.equals(valueProperty.get(), newText)) { - valueProperty.set(newText); - } - }); - }); - } } diff --git a/src/main/java/dev/railroadide/railroad/project/onboarding/impl/ForgeProjectOnboarding.java b/src/main/java/dev/railroadide/railroad/project/onboarding/impl/ForgeProjectOnboarding.java index a1639929..99aeedba 100644 --- a/src/main/java/dev/railroadide/railroad/project/onboarding/impl/ForgeProjectOnboarding.java +++ b/src/main/java/dev/railroadide/railroad/project/onboarding/impl/ForgeProjectOnboarding.java @@ -1,9 +1,6 @@ package dev.railroadide.railroad.project.onboarding.impl; import dev.railroadide.core.form.FormComponent; -import dev.railroadide.core.form.FormComponentBuilder; -import dev.railroadide.core.form.ui.InformativeLabeledHBox; -import dev.railroadide.core.project.License; import dev.railroadide.core.project.ProjectData; import dev.railroadide.core.project.creation.ProjectCreationService; import dev.railroadide.core.project.creation.ProjectServiceRegistry; @@ -22,62 +19,33 @@ import dev.railroadide.railroad.project.onboarding.flow.OnboardingFlow; import dev.railroadide.railroad.project.onboarding.step.OnboardingFormStep; import dev.railroadide.railroad.project.onboarding.step.OnboardingStep; -import dev.railroadide.railroad.settings.Settings; -import dev.railroadide.railroad.settings.handler.SettingsHandler; import dev.railroadide.railroad.switchboard.SwitchboardRepositories; -import dev.railroadide.railroad.utility.ExpiringCache; -import dev.railroadide.railroad.welcome.project.ui.widget.StarableListCell; -import javafx.application.Platform; -import javafx.beans.binding.Bindings; -import javafx.beans.binding.BooleanBinding; -import javafx.beans.property.*; -import javafx.beans.value.ChangeListener; import javafx.collections.FXCollections; import javafx.collections.ObservableList; -import javafx.scene.Node; import javafx.scene.Scene; -import javafx.scene.control.ComboBox; -import javafx.scene.control.TextField; -import org.apache.commons.collections.ListUtils; -import java.nio.file.Path; -import java.time.Duration; import java.util.*; import java.util.concurrent.*; -import java.util.concurrent.atomic.AtomicLong; -import java.util.function.Consumer; -import java.util.function.Function; // TODO: Make it so the display test and client side only options are only shown for versions that support it // TODO: Make it so the display test and client side only options are in their own steps // TODO: Fix the comboboxes not being immediately populated and instead having the data fetched completely async -public class ForgeProjectOnboarding { - private static final ExpiringCache> FORGE_MINECRAFT_VERSIONS_CACHE = new ExpiringCache<>(Duration.ofHours(3)); - +public class ForgeProjectOnboarding extends Onboarding { private final ExecutorService executor = Executors.newFixedThreadPool(4); public void start(Scene scene) { var flow = OnboardingFlow.builder() - .addStep("project_details", this::createProjectDetailsStep) - .addStep("maven_coordinates", this::createMavenCoordinatesStep) + .addStep("project_details", this::createProjectDetailsStep) // name, loc + .addStep("maven_coordinates", this::createMavenCoordinatesStep) // groupid, artifact id, version .addStep("minecraft_version", this::createMinecraftVersionStep) - .addStep("forge_version", this::createForgeVersionStep) - .addStep("mapping_channel", this::createMappingChannelStep) + .addStep("mapping_channel", this::createMappingChannelStep) // parchment or mojmaps .addStep("mapping_version", this::createMappingVersionStep) - .addStep("mod_details", this::createModDetailsStep) + .addStep("forge", this::createForgeStep) // forge version + .addStep("mod_details", this::createModDetailsStep) // id, name, class .addStep("license", this::createLicenseStep) - .addStep("git", this::createGitStep) - .addStep("optional_details", this::createOptionalDetailsStep) + .addStep("git", this::createGitStep) // make git repo + .addStep("optional_details", this::createOptionalDetailsStep) // author, desc, website, sources .firstStep("project_details") - .addTransition("project_details", "maven_coordinates") - .addTransition("maven_coordinates", "minecraft_version") - .addTransition("minecraft_version", "forge_version") - .addTransition("forge_version", "mapping_channel") - .addTransition("mapping_channel", "mapping_version") - .addTransition("mapping_version", "mod_details") - .addTransition("mod_details", "license") - .addTransition("license", "git") - .addTransition("git", "optional_details") .build(); var process = OnboardingProcess.createBasic( @@ -89,77 +57,43 @@ public void start(Scene scene) { process.run(scene); } - private void onFinish(OnboardingContext ctx, Scene scene) { - executor.shutdown(); + @Override + protected void onFinish(OnboardingContext ctx, Scene scene) { + this.executor.shutdown(); var data = new ProjectData(); data.set(ProjectData.DefaultKeys.TYPE, ProjectTypeRegistry.FORGE); data.set(ProjectData.DefaultKeys.NAME, ctx.get(ProjectData.DefaultKeys.NAME)); data.set(ProjectData.DefaultKeys.PATH, ctx.get(ProjectData.DefaultKeys.PATH)); - data.set(ProjectData.DefaultKeys.INIT_GIT, Boolean.TRUE.equals(ctx.get(ProjectData.DefaultKeys.INIT_GIT))); - data.set(ProjectData.DefaultKeys.LICENSE, ctx.get(ProjectData.DefaultKeys.LICENSE)); + data.set(ProjectData.DefaultKeys.INIT_GIT, ctx.get(ProjectData.DefaultKeys.INIT_GIT)); - if (ctx.contains(ProjectData.DefaultKeys.LICENSE_CUSTOM)) { + data.set(ProjectData.DefaultKeys.LICENSE, ctx.get(ProjectData.DefaultKeys.LICENSE)); + // TODO: Get rid of this and move into CustomLicense + if (ctx.contains(ProjectData.DefaultKeys.LICENSE_CUSTOM)) data.set(ProjectData.DefaultKeys.LICENSE_CUSTOM, ctx.get(ProjectData.DefaultKeys.LICENSE_CUSTOM)); - } data.set(MinecraftProjectKeys.MINECRAFT_VERSION, ctx.get(MinecraftProjectKeys.MINECRAFT_VERSION)); - data.set(ForgeProjectKeys.FORGE_VERSION, ctx.get(ForgeProjectKeys.FORGE_VERSION)); - data.set(MinecraftProjectKeys.MAPPING_CHANNEL, ctx.get(MinecraftProjectKeys.MAPPING_CHANNEL)); - data.set(MinecraftProjectKeys.MAPPING_VERSION, ctx.get(MinecraftProjectKeys.MAPPING_VERSION)); + + if (ctx.contains(ForgeProjectKeys.FORGE_VERSION)) + data.set(ForgeProjectKeys.FORGE_VERSION, ctx.get(ForgeProjectKeys.FORGE_VERSION)); + data.set(MinecraftProjectKeys.MOD_ID, ctx.get(MinecraftProjectKeys.MOD_ID)); data.set(MinecraftProjectKeys.MOD_NAME, ctx.get(MinecraftProjectKeys.MOD_NAME)); data.set(MinecraftProjectKeys.MAIN_CLASS, ctx.get(MinecraftProjectKeys.MAIN_CLASS)); - data.set(ForgeProjectKeys.USE_MIXINS, Boolean.TRUE.equals(ctx.get(ForgeProjectKeys.USE_MIXINS))); - data.set(ForgeProjectKeys.USE_ACCESS_TRANSFORMER, Boolean.TRUE.equals(ctx.get(ForgeProjectKeys.USE_ACCESS_TRANSFORMER))); - data.set(ForgeProjectKeys.GEN_RUN_FOLDERS, Boolean.TRUE.equals(ctx.get(ForgeProjectKeys.GEN_RUN_FOLDERS))); - - if (ctx.contains(ProjectData.DefaultKeys.AUTHOR)) { - String author = ctx.get(ProjectData.DefaultKeys.AUTHOR); - if (!isNullOrBlank(author)) { - data.set(ProjectData.DefaultKeys.AUTHOR, author); - } - } - - if (ctx.contains(ProjectData.DefaultKeys.CREDITS)) { - String credits = ctx.get(ProjectData.DefaultKeys.CREDITS); - if (!isNullOrBlank(credits)) { - data.set(ProjectData.DefaultKeys.CREDITS, credits); - } - } - - if (ctx.contains(ProjectData.DefaultKeys.DESCRIPTION)) { - String description = ctx.get(ProjectData.DefaultKeys.DESCRIPTION); - if (!isNullOrBlank(description)) { - data.set(ProjectData.DefaultKeys.DESCRIPTION, description); - } - } - - if (ctx.contains(ProjectData.DefaultKeys.ISSUES_URL)) { - String issuesUrl = ctx.get(ProjectData.DefaultKeys.ISSUES_URL); - if (!isNullOrBlank(issuesUrl)) { - data.set(ProjectData.DefaultKeys.ISSUES_URL, issuesUrl); - } - } - - if (ctx.contains(ForgeProjectKeys.UPDATE_JSON_URL)) { - String updateJson = ctx.get(ForgeProjectKeys.UPDATE_JSON_URL); - if (!isNullOrBlank(updateJson)) { - data.set(ForgeProjectKeys.UPDATE_JSON_URL, updateJson); - } - } - - if (ctx.contains(ForgeProjectKeys.DISPLAY_URL)) { - String displayUrl = ctx.get(ForgeProjectKeys.DISPLAY_URL); - if (!isNullOrBlank(displayUrl)) { - data.set(ForgeProjectKeys.DISPLAY_URL, displayUrl); - } - } + data.set(ForgeProjectKeys.CLIENT_SIDE_ONLY, ctx.get(ForgeProjectKeys.CLIENT_SIDE_ONLY)); + data.set(MinecraftProjectKeys.MAPPING_CHANNEL, ctx.get(MinecraftProjectKeys.MAPPING_CHANNEL)); + data.set(MinecraftProjectKeys.MAPPING_VERSION, ctx.get(MinecraftProjectKeys.MAPPING_VERSION)); - DisplayTest displayTest = Optional.ofNullable((DisplayTest) ctx.get(ForgeProjectKeys.DISPLAY_TEST)) - .orElse(DisplayTest.MATCH_VERSION); - data.set(ForgeProjectKeys.DISPLAY_TEST, displayTest); - data.set(ForgeProjectKeys.CLIENT_SIDE_ONLY, Boolean.TRUE.equals(ctx.get(ForgeProjectKeys.CLIENT_SIDE_ONLY))); + if (ctx.contains(ProjectData.DefaultKeys.AUTHOR)) + data.set(ProjectData.DefaultKeys.AUTHOR, ctx.get(ProjectData.DefaultKeys.AUTHOR)); + if (ctx.contains(ProjectData.DefaultKeys.DESCRIPTION)) + data.set(ProjectData.DefaultKeys.DESCRIPTION, ctx.get(ProjectData.DefaultKeys.DESCRIPTION)); + if (ctx.contains(ProjectData.DefaultKeys.ISSUES_URL)) + data.set(ProjectData.DefaultKeys.ISSUES_URL, ctx.get(ProjectData.DefaultKeys.ISSUES_URL)); + if (ctx.contains(ProjectData.DefaultKeys.HOMEPAGE_URL)) + data.set(ProjectData.DefaultKeys.HOMEPAGE_URL, ctx.get(ProjectData.DefaultKeys.HOMEPAGE_URL)); + if (ctx.contains(ProjectData.DefaultKeys.SOURCES_URL)) + data.set(ProjectData.DefaultKeys.SOURCES_URL, ctx.get(ProjectData.DefaultKeys.SOURCES_URL)); data.set(MavenProjectKeys.GROUP_ID, ctx.get(MavenProjectKeys.GROUP_ID)); data.set(MavenProjectKeys.ARTIFACT_ID, ctx.get(MavenProjectKeys.ARTIFACT_ID)); @@ -174,188 +108,16 @@ private void onFinish(OnboardingContext ctx, Scene scene) { serviceRegistry ), creationPane.getContext())); - scene.setRoot(creationPane); - } - - private OnboardingStep createProjectDetailsStep() { - return OnboardingFormStep.builder() - .id("project_details") - .title("railroad.project.creation.project_details.title") - .description("railroad.project.creation.project_details.description") - .appendSection("railroad.project.creation.section.project", - described( - FormComponent.textField(ProjectData.DefaultKeys.NAME, "railroad.project.creation.name") - .required() - .promptText("railroad.project.creation.name.prompt") - .validator(ProjectValidators::validateProjectName), - "railroad.project.creation.name.info"), - described( - FormComponent.directoryChooser(ProjectData.DefaultKeys.PATH, "railroad.project.creation.location") - .required() - .defaultPath(System.getProperty("user.home")) - .validator(ProjectValidators::validatePath), - value -> { - if (value == null) - return null; - - String text = value.toString(); - return text.isBlank() ? null : Path.of(text); - }, - value -> { - if (value == null) - return null; - - return value instanceof Path path ? path.toAbsolutePath().toString() : value.toString(); - }, - "railroad.project.creation.location.info")) - .build(); - } - - private OnboardingStep createMavenCoordinatesStep() { - StringProperty artifactId = new SimpleStringProperty(); - String configuredGroupId = SettingsHandler.getValue(Settings.DEFAULT_PROJECT_GROUP_ID); - String configuredVersion = SettingsHandler.getValue(Settings.DEFAULT_PROJECT_VERSION); - String defaultGroupId = isNullOrBlank(configuredGroupId) ? "" : configuredGroupId; - String defaultVersion = isNullOrBlank(configuredVersion) ? "1.0.0" : configuredVersion; - - return OnboardingFormStep.builder() - .id("maven_coordinates") - .title("railroad.project.creation.maven_coordinates.title") - .description("railroad.project.creation.maven_coordinates.description") - .appendSection("railroad.project.creation.section.maven_coordinates", - described( - FormComponent.textField(MavenProjectKeys.GROUP_ID, "railroad.project.creation.group_id") - .required() - .promptText("railroad.project.creation.group_id.prompt") - .text(() -> defaultGroupId) - .validator(ProjectValidators::validateGroupId), - "railroad.project.creation.group_id.info"), - described( - FormComponent.textField(MavenProjectKeys.ARTIFACT_ID, "railroad.project.creation.artifact_id") - .required() - .promptText("railroad.project.creation.artifact_id.prompt") - .text(artifactId::get) - .validator(ProjectValidators::validateArtifactId), - "railroad.project.creation.artifact_id.info"), - described( - FormComponent.textField(MavenProjectKeys.VERSION, "railroad.project.creation.version") - .required() - .promptText("railroad.project.creation.version.prompt") - .text(() -> defaultVersion) - .validator(ProjectValidators::validateVersion), - "railroad.project.creation.version.info")) - .onEnter(ctx -> { - String projectName = ctx.get(ProjectData.DefaultKeys.NAME); - if (projectName != null) { - String defaultArtifactId = ProjectValidators.projectNameToArtifactId(projectName); - if (isNullOrBlank(artifactId.get())) { - artifactId.set(defaultArtifactId); - } - } - }) - .build(); - } - - private OnboardingStep createMinecraftVersionStep() { - ObservableList availableVersions = FXCollections.observableArrayList(); - AtomicLong nextInvalidationTime = new AtomicLong(0L); - - return OnboardingFormStep.builder() - .id("minecraft_version") - .title("railroad.project.creation.minecraft_version.title") - .description("railroad.project.creation.minecraft_version.description") - .appendSection("railroad.project.creation.section.minecraft_version", - described( - FormComponent.comboBox(MinecraftProjectKeys.MINECRAFT_VERSION, "railroad.project.creation.minecraft_version", MinecraftVersion.class) - .items(() -> availableVersions) - .defaultValue(() -> determineDefaultMinecraftVersion(availableVersions)) - .keyFunction(MinecraftVersion::id) - .valueOfFunction(ForgeProjectOnboarding::getMinecraftVersion) - .required() - .translate(false), - "railroad.project.creation.minecraft_version.info")) - .onEnter(ctx -> { - long now = System.currentTimeMillis(); - if (availableVersions.isEmpty() || now > nextInvalidationTime.get()) { - FORGE_MINECRAFT_VERSIONS_CACHE.getIfPresent().ifPresent(values -> - Platform.runLater(() -> { - availableVersions.setAll(values); - ctx.markForRefresh(MinecraftProjectKeys.MINECRAFT_VERSION); - })); - - resolveForgeMinecraftVersions().whenComplete((versions, throwable) -> { - if (throwable != null) { - Railroad.LOGGER.error("Failed to fetch Minecraft versions for Forge", throwable); - return; - } - - Platform.runLater(() -> { - availableVersions.setAll(versions); - ctx.markForRefresh(MinecraftProjectKeys.MINECRAFT_VERSION); - }); - }); - - nextInvalidationTime.set(now + TimeUnit.MINUTES.toMillis(5)); - } - }) - .build(); - } - - private OnboardingStep createForgeVersionStep() { - ObservableList availableVersions = FXCollections.observableArrayList(); - StringProperty latestForgeVersion = new SimpleStringProperty(); - - return OnboardingFormStep.builder() - .id("forge_version") - .title("railroad.project.creation.forge_version.title") - .description("railroad.project.creation.forge_version.description") - .appendSection("railroad.project.creation.section.forge_version", - described( - FormComponent.comboBox(ForgeProjectKeys.FORGE_VERSION, "railroad.project.creation.forge_version", String.class) - .required() - .items(() -> availableVersions) - .defaultValue(() -> { - String latest = latestForgeVersion.get(); - if (latest != null && availableVersions.contains(latest)) - return latest; - - if (!availableVersions.isEmpty()) - return availableVersions.getFirst(); + data.set(ForgeProjectKeys.USE_MIXINS, true); + data.set(ForgeProjectKeys.USE_ACCESS_TRANSFORMER, true); + data.set(ForgeProjectKeys.DISPLAY_TEST, true); + data.set(ForgeProjectKeys.GEN_RUN_FOLDERS, true); - return null; - }) - .translate(false) - .cellFactory(param -> new StarableListCell<>( - version -> isRecommendedForgeVersion(version, latestForgeVersion.get()), - latest -> Objects.equals(latest, latestForgeVersion.get()), - Function.identity())) - .buttonCell(new StarableListCell<>( - version -> isRecommendedForgeVersion(version, latestForgeVersion.get()), - latest -> Objects.equals(latest, latestForgeVersion.get()), - Function.identity())), - "railroad.project.creation.forge_version.info")) - .onEnter(ctx -> { - MinecraftVersion minecraftVersion = ctx.get(MinecraftProjectKeys.MINECRAFT_VERSION); - - fetchForgeVersions(minecraftVersion).whenComplete((payload, throwable) -> { - if (throwable != null) { - Railroad.LOGGER.error("Failed to fetch Forge versions for Minecraft {}", minecraftVersion, throwable); - return; - } - - Platform.runLater(() -> { - availableVersions.setAll(payload.versions()); - latestForgeVersion.set(payload.latest()); - ctx.markForRefresh(ForgeProjectKeys.FORGE_VERSION); - }); - }); - }) - .build(); + scene.setRoot(creationPane); } private OnboardingStep createMappingChannelStep() { ObservableList availableChannels = FXCollections.observableArrayList(); - return OnboardingFormStep.builder() .id("mapping_channel") .title("railroad.project.creation.mapping_channel.title") @@ -365,28 +127,18 @@ private OnboardingStep createMappingChannelStep() { FormComponent.comboBox(MinecraftProjectKeys.MAPPING_CHANNEL, "railroad.project.creation.mapping_channel", MappingChannel.class) .required() .items(() -> availableChannels) - .defaultValue(() -> { - if (availableChannels.contains(MappingChannelRegistry.MOJMAP)) - return MappingChannelRegistry.MOJMAP; - - if (!availableChannels.isEmpty()) - return availableChannels.getFirst(); - - return null; - }) + .defaultValue(() -> MappingChannelRegistry.PARCHMENT) .keyFunction(MappingChannel::id) .valueOfFunction(MappingChannel.REGISTRY::get) .defaultDisplayNameFunction(MappingChannel::translationKey) .translate(true), "railroad.project.creation.mapping_channel.info")) .onEnter(ctx -> { - MinecraftVersion minecraftVersion = ctx.get(MinecraftProjectKeys.MINECRAFT_VERSION); - if (minecraftVersion != null) { - List channels = MappingChannelRegistry.findValidMappingChannels(minecraftVersion); - Platform.runLater(() -> { - availableChannels.setAll(channels); - ctx.markForRefresh(MinecraftProjectKeys.MAPPING_CHANNEL); - }); + MinecraftVersion mcVersion = ctx.get(MinecraftProjectKeys.MINECRAFT_VERSION); + if (mcVersion != null) { + availableChannels.clear(); + availableChannels.setAll(MappingChannelRegistry.findValidMappingChannels(mcVersion)); + ctx.markForRefresh(MinecraftProjectKeys.MAPPING_CHANNEL); } }) .build(); @@ -394,7 +146,6 @@ private OnboardingStep createMappingChannelStep() { private OnboardingStep createMappingVersionStep() { ObservableList availableVersions = FXCollections.observableArrayList(); - return OnboardingFormStep.builder() .id("mapping_version") .title("railroad.project.creation.mapping_version.title") @@ -406,400 +157,75 @@ private OnboardingStep createMappingVersionStep() { .items(() -> availableVersions) .defaultValue(() -> { if (!availableVersions.isEmpty()) - return availableVersions.getLast(); + return availableVersions.getFirst(); return null; }) .translate(false), "railroad.project.creation.mapping_version.info")) .onEnter(ctx -> { - MinecraftVersion minecraftVersion = ctx.get(MinecraftProjectKeys.MINECRAFT_VERSION); - MappingChannel mappingChannel = ctx.get(MinecraftProjectKeys.MAPPING_CHANNEL); - if (minecraftVersion != null && mappingChannel != null) { - List versions = mappingChannel.listVersionsFor(minecraftVersion); - Platform.runLater(() -> { - availableVersions.setAll(versions); - ctx.markForRefresh(MinecraftProjectKeys.MAPPING_VERSION); - }); - } - }) - .build(); - } - - private OnboardingStep createModDetailsStep() { - StringProperty modIdProperty = new SimpleStringProperty(); - StringProperty modNameProperty = new SimpleStringProperty(); - StringProperty mainClassProperty = new SimpleStringProperty(); - - ObjectProperty modIdField = new SimpleObjectProperty<>(); - ObjectProperty modNameField = new SimpleObjectProperty<>(); - ObjectProperty mainClassField = new SimpleObjectProperty<>(); - - bindTextField(modIdProperty, modIdField); - bindTextField(modNameProperty, modNameField); - bindTextField(mainClassProperty, mainClassField); - - return OnboardingFormStep.builder() - .id("mod_details") - .title("railroad.project.creation.mod_details.title") - .description("railroad.project.creation.mod_details.description") - .appendSection("railroad.project.creation.section.mod_details", - described( - FormComponent.textField(MinecraftProjectKeys.MOD_ID, "railroad.project.creation.mod_id") - .required() - .promptText("railroad.project.creation.mod_id.prompt") - .text(modIdProperty::get) - .bindTextFieldTo(modIdField) - .validator(ProjectValidators::validateModId), - "railroad.project.creation.mod_id.info"), - described( - FormComponent.textField(MinecraftProjectKeys.MOD_NAME, "railroad.project.creation.mod_name") - .required() - .promptText("railroad.project.creation.mod_name.prompt") - .text(modNameProperty::get) - .bindTextFieldTo(modNameField) - .validator(ProjectValidators::validateModName), - "railroad.project.creation.mod_name.info"), - described( - FormComponent.textField(MinecraftProjectKeys.MAIN_CLASS, "railroad.project.creation.main_class") - .required() - .promptText("railroad.project.creation.main_class.prompt") - .text(mainClassProperty::get) - .bindTextFieldTo(mainClassField) - .validator(ProjectValidators::validateMainClass), - "railroad.project.creation.main_class.info")) - .appendSection("railroad.project.creation.section.forge_options", - described( - FormComponent.checkBox(ForgeProjectKeys.USE_MIXINS, "railroad.project.creation.use_mixins"), - "railroad.project.creation.use_mixins.info"), - described( - FormComponent.checkBox(ForgeProjectKeys.USE_ACCESS_TRANSFORMER, "railroad.project.creation.use_access_transformer"), - "railroad.project.creation.use_access_transformer.info"), - described( - FormComponent.checkBox(ForgeProjectKeys.GEN_RUN_FOLDERS, "railroad.project.creation.gen_run_folders"), - "railroad.project.creation.gen_run_folders.info")) - .onEnter(ctx -> { - String projectName = ctx.get(ProjectData.DefaultKeys.NAME); - - if (!isNullOrBlank(projectName)) { - modIdProperty.set(ProjectValidators.projectNameToModId(projectName)); - } - - if (!isNullOrBlank(projectName)) { - modNameProperty.set(projectName); - } - - if (!isNullOrBlank(projectName)) { - String mainClassName = ProjectValidators.projectNameToMainClass(projectName); - mainClassProperty.set(isNullOrBlank(mainClassName) ? "" : mainClassName); + MinecraftVersion mcVersion = ctx.get(MinecraftProjectKeys.MINECRAFT_VERSION); + MappingChannel channel = ctx.get(MinecraftProjectKeys.MAPPING_CHANNEL); + if (mcVersion != null && channel != null) { + List newVersions = channel.listVersionsFor(mcVersion); + availableVersions.clear(); + availableVersions.setAll(newVersions); + ctx.markForRefresh(MinecraftProjectKeys.MAPPING_VERSION); } }) .build(); } - private OnboardingStep createLicenseStep() { - ObservableList availableLicenses = FXCollections.observableArrayList(); - ObjectProperty> licenseComboBox = new SimpleObjectProperty<>(); - BooleanProperty showCustomLicense = new SimpleBooleanProperty(false); - ChangeListener licenseSelectionListener = (observable, oldValue, newValue) -> - showCustomLicense.set(newValue == LicenseRegistry.CUSTOM); - - licenseComboBox.addListener((observable, oldValue, newValue) -> { - if (oldValue != null) { - oldValue.valueProperty().removeListener(licenseSelectionListener); - } - - if (newValue != null) { - showCustomLicense.set(newValue.getValue() == LicenseRegistry.CUSTOM); - newValue.valueProperty().addListener(licenseSelectionListener); - } else { - showCustomLicense.set(false); - } - }); - - BooleanBinding customLicenseVisible = Bindings.createBooleanBinding(showCustomLicense::get, showCustomLicense); - + private OnboardingStep createForgeStep() { + ObservableList availableVersions = FXCollections.observableArrayList(); return OnboardingFormStep.builder() - .id("license") - .title("railroad.project.creation.license.title") - .description("railroad.project.creation.license.description") - .appendSection("railroad.project.creation.section.license", + .id("forge") + .title("railroad.project.creation.forge.title") + .description("railroad.project.creation.forge.description") + .appendSection("railroad.project.creation.section.forge", described( - FormComponent.comboBox(ProjectData.DefaultKeys.LICENSE, "railroad.project.creation.license", License.class) - .required() - .bindComboBoxTo(licenseComboBox) - .keyFunction(License::getSpdxId) - .valueOfFunction(License::fromSpdxId) - .defaultDisplayNameFunction(License::getName) - .translate(false) - .items(() -> availableLicenses) + FormComponent.comboBox(ForgeProjectKeys.FORGE_VERSION, "railroad.project.creation.forge", String.class) + .items(() -> availableVersions) .defaultValue(() -> { - if (availableLicenses.contains(LicenseRegistry.LGPL)) - return LicenseRegistry.LGPL; - - if (!availableLicenses.isEmpty()) - return availableLicenses.getFirst(); + if (!availableVersions.isEmpty()) + return availableVersions.getFirst(); return null; - }), - "railroad.project.creation.license.info"), - described( - FormComponent.textField(ProjectData.DefaultKeys.LICENSE_CUSTOM, "railroad.project.creation.license.custom") - .visible(customLicenseVisible) - .promptText("railroad.project.creation.license.custom.prompt") - .validator(ProjectValidators::validateCustomLicense), - "railroad.project.creation.license.custom.info")) + }) + .translate(false), + "railroad.project.creation.forge.info")) .onEnter(ctx -> { - List newValues = License.REGISTRY.values() - .stream() - .sorted(Comparator.comparing(License::getName)) - .toList(); - - if (availableLicenses.size() != newValues.size() || !ListUtils.isEqualList(availableLicenses, newValues)) { - availableLicenses.clear(); - availableLicenses.addAll(newValues); - ctx.markForRefresh(ProjectData.DefaultKeys.LICENSE); + MinecraftVersion mcVersion = ctx.get(MinecraftProjectKeys.MINECRAFT_VERSION); + if (mcVersion != null) { + try { + CompletableFuture> versionsFuture = SwitchboardRepositories.FORGE.getVersionsFor(mcVersion.id()); + List versions = versionsFuture.get(); + availableVersions.clear(); + availableVersions.addAll(versions); + ctx.markForRefresh(ForgeProjectKeys.FORGE_VERSION); + } catch (ExecutionException | InterruptedException exception) { + Railroad.LOGGER.error("Failed to fetch Forge versions for Minecraft {}", mcVersion.id(), exception); + } } }) .build(); } - private OnboardingStep createGitStep() { - return OnboardingFormStep.builder() - .id("git") - .title("railroad.project.creation.git.title") - .description("railroad.project.creation.git.description") - .appendSection("railroad.project.creation.section.git", - described( - FormComponent.checkBox(ProjectData.DefaultKeys.INIT_GIT, "railroad.project.creation.init_git") - .selected(true), - "railroad.project.creation.init_git.info")) - .build(); - } - - private OnboardingStep createOptionalDetailsStep() { - String configuredAuthor = SettingsHandler.getValue(Settings.DEFAULT_PROJECT_AUTHOR); - String defaultAuthor = !isNullOrBlank(configuredAuthor) - ? configuredAuthor - : Optional.ofNullable(System.getProperty("user.name")) - .filter(name -> !isNullOrBlank(name)) - .orElse(""); - - return OnboardingFormStep.builder() - .id("optional_details") - .title("railroad.project.creation.optional_details.title") - .description("railroad.project.creation.optional_details.description") - .appendSection("railroad.project.creation.section.optional_details", - described( - FormComponent.textField(ProjectData.DefaultKeys.AUTHOR, "railroad.project.creation.author") - .text(() -> defaultAuthor) - .promptText("railroad.project.creation.author.prompt") - .validator(ProjectValidators::validateAuthor), - "railroad.project.creation.author.info"), - described( - FormComponent.textField(ProjectData.DefaultKeys.CREDITS, "railroad.project.creation.credits") - .promptText("railroad.project.creation.credits.prompt") - .validator(ProjectValidators::validateCredits), - "railroad.project.creation.credits.info"), - described( - FormComponent.textArea(ProjectData.DefaultKeys.DESCRIPTION, "railroad.project.creation.description") - .promptText("railroad.project.creation.description.prompt") - .validator(ProjectValidators::validateDescription), - "railroad.project.creation.description.info"), - described( - FormComponent.textField(ProjectData.DefaultKeys.ISSUES_URL, "railroad.project.creation.issues_url") - .promptText("railroad.project.creation.issues_url.prompt") - .validator(ProjectValidators::validateIssues), - "railroad.project.creation.issues_url.info"), - described( - FormComponent.textField(ForgeProjectKeys.UPDATE_JSON_URL, "railroad.project.creation.update_json_url") - .promptText("railroad.project.creation.update_json_url.prompt") - .validator(ProjectValidators::validateUpdateJsonUrl), - "railroad.project.creation.update_json_url.info"), - described( - FormComponent.textField(ForgeProjectKeys.DISPLAY_URL, "railroad.project.creation.display_url") - .promptText("railroad.project.creation.display_url.prompt") - .validator(field -> ProjectValidators.validateGenericUrl(field, "display_url")), - "railroad.project.creation.display_url.info"), - described( - FormComponent.comboBox(ForgeProjectKeys.DISPLAY_TEST, "railroad.project.creation.display_test", DisplayTest.class) - .items(() -> Arrays.asList(DisplayTest.values())) - .defaultValue(() -> DisplayTest.MATCH_VERSION) - .keyFunction(DisplayTest::name) - .valueOfFunction(DisplayTest::valueOf) - .translate(false), - "railroad.project.creation.display_test.info"), - described( - FormComponent.checkBox(ForgeProjectKeys.CLIENT_SIDE_ONLY, "railroad.project.creation.client_side_only"), - "railroad.project.creation.client_side_only.info")) - .build(); - } - - private static OnboardingFormStep.ComponentSpec described(FormComponentBuilder builder, String descriptionKey) { - return OnboardingFormStep.component(builder, createDescriptionCustomizer(descriptionKey)); - } - - private static OnboardingFormStep.ComponentSpec described(FormComponentBuilder builder, Function transformer, Function reverseTransformer, String descriptionKey) { - return OnboardingFormStep.component(builder, builder != null ? builder.dataKey() : null, transformer, reverseTransformer, createDescriptionCustomizer(descriptionKey)); - } - - private static Consumer> createDescriptionCustomizer(String descriptionKey) { - if (isNullOrBlank(descriptionKey)) - return null; - - return component -> attachDescription(component, descriptionKey); - } - - private static void attachDescription(FormComponent component, String descriptionKey) { - if (component == null || isNullOrBlank(descriptionKey)) - return; - - Consumer applyToNode = node -> { - if (node instanceof InformativeLabeledHBox informative) { - boolean exists = informative.getInformationLabels().stream() - .anyMatch(label -> descriptionKey.equals(label.getKey())); - if (!exists) { - informative.addInformationLabel(descriptionKey); - } - } - }; - - Node currentNode = component.componentProperty().get(); - if (currentNode != null) { - applyToNode.accept(currentNode); - } - - component.componentProperty().addListener((observable, oldValue, newValue) -> { - if (newValue != null) { - applyToNode.accept(newValue); - } - }); - } + @Override + protected List getMinecraftVersions() { - private static MinecraftVersion getMinecraftVersion(String string) { + // TODO: get minecraft versions available for forge directly from switchboard rather than version filter try { - return SwitchboardRepositories.MINECRAFT.getVersionSync(string).orElse(null); + return SwitchboardRepositories.MINECRAFT.getAllVersionsSync().stream() + .filter(v -> { + String id = v.id(); + return !id.contains("-") && !id.contains("w"); + }) + .sorted(Comparator.reverseOrder()) + .toList(); } catch (ExecutionException | InterruptedException exception) { - Railroad.LOGGER.error("Failed to fetch Minecraft version {}", string, exception); - return null; + Railroad.LOGGER.error("Failed to fetch Minecraft versions", exception); + return Collections.emptyList(); } } - - private static CompletableFuture> resolveForgeMinecraftVersions() { - return FORGE_MINECRAFT_VERSIONS_CACHE.getAsync(() -> - SwitchboardRepositories.FORGE.getAllVersions() - .thenApply(versions -> versions.stream() - .map(ForgeProjectOnboarding::extractMinecraftVersionId) - .flatMap(Optional::stream) - .map(ForgeProjectOnboarding::lookupMinecraftVersion) - .flatMap(Optional::stream) - .distinct() - .sorted(Comparator.reverseOrder()) - .toList()) - ); - } - - private static CompletableFuture fetchForgeVersions(MinecraftVersion version) { - if (version == null) - return CompletableFuture.completedFuture(new ForgeVersionsPayload(null, List.of(), null)); - - String minecraftId = version.id(); - CompletableFuture> versionsFuture = SwitchboardRepositories.FORGE.getVersionsFor(minecraftId); - CompletableFuture latestFuture = SwitchboardRepositories.FORGE.getLatestVersionFor(minecraftId); - - return versionsFuture.thenCombine(latestFuture, (versions, latest) -> - new ForgeVersionsPayload(version, versions == null ? List.of() : versions, latest)) - .exceptionally(throwable -> { - Railroad.LOGGER.error("Failed to fetch Forge versions for Minecraft {}", version, throwable); - return new ForgeVersionsPayload(version, List.of(), null); - }); - } - - private static MinecraftVersion determineDefaultMinecraftVersion(List versions) { - if (versions == null || versions.isEmpty()) - return null; - - return versions.stream() - .filter(version -> version != null && version.getType() == MinecraftVersion.Type.RELEASE) - .findFirst() - .orElseGet(versions::getFirst); - } - - private static Optional lookupMinecraftVersion(String versionId) { - try { - return SwitchboardRepositories.MINECRAFT.getVersionSync(versionId); - } catch (ExecutionException exception) { - Railroad.LOGGER.error("Failed to fetch Minecraft version {}", versionId, exception); - } catch (InterruptedException exception) { - Thread.currentThread().interrupt(); - Railroad.LOGGER.error("Interrupted while fetching Minecraft version {}", versionId, exception); - } - - return Optional.empty(); - } - - private static Optional extractMinecraftVersionId(String forgeVersion) { - if (forgeVersion == null || forgeVersion.isBlank()) - return Optional.empty(); - - String lower = forgeVersion.toLowerCase(Locale.ROOT); - if (lower.contains("25w14craftmine")) - return Optional.of("25w14craftmine"); - - int lastDash = forgeVersion.indexOf('-'); - if (lastDash <= 0) - return Optional.empty(); - - String base = forgeVersion.substring(0, lastDash); - if (base.isBlank()) - return Optional.empty(); - - return Optional.of(base); - } - - private static boolean isRecommendedForgeVersion(String version, String latest) { - return version != null && Objects.equals(version, latest) && !isForgePrerelease(version); - } - - private static boolean isForgePrerelease(String version) { - if (version == null) - return false; - - String lower = version.toLowerCase(Locale.ROOT); - return lower.contains("beta") || lower.contains("alpha") || lower.contains("rc") || lower.contains("25w14craftmine"); - } - - private static boolean isNullOrBlank(String value) { - return value == null || value.isBlank(); - } - - private static void bindTextField(StringProperty valueProperty, ObjectProperty fieldProperty) { - Objects.requireNonNull(valueProperty, "valueProperty"); - Objects.requireNonNull(fieldProperty, "fieldProperty"); - - valueProperty.addListener((obs, oldValue, newValue) -> { - TextField field = fieldProperty.get(); - if (field != null && !Objects.equals(field.getText(), newValue)) { - field.setText(newValue); - } - }); - - fieldProperty.addListener((obs, oldField, newField) -> { - if (newField == null) - return; - - if (!Objects.equals(newField.getText(), valueProperty.get())) { - newField.setText(valueProperty.get()); - } - - newField.textProperty().addListener((textObs, oldText, newText) -> { - if (!Objects.equals(valueProperty.get(), newText)) { - valueProperty.set(newText); - } - }); - }); - } - - private record ForgeVersionsPayload(MinecraftVersion contextVersion, List versions, String latest) { - } } diff --git a/src/main/java/dev/railroadide/railroad/project/onboarding/impl/NeoforgeProjectOnboarding.java b/src/main/java/dev/railroadide/railroad/project/onboarding/impl/NeoforgeProjectOnboarding.java index 15ad35ef..aaf439c2 100644 --- a/src/main/java/dev/railroadide/railroad/project/onboarding/impl/NeoforgeProjectOnboarding.java +++ b/src/main/java/dev/railroadide/railroad/project/onboarding/impl/NeoforgeProjectOnboarding.java @@ -9,11 +9,13 @@ import dev.railroadide.core.project.creation.ProjectServiceRegistry; import dev.railroadide.core.project.creation.service.GradleService; import dev.railroadide.core.project.minecraft.MappingChannel; +import dev.railroadide.core.switchboard.pojo.FabricLoaderVersion; import dev.railroadide.core.switchboard.pojo.MinecraftVersion; import dev.railroadide.railroad.Railroad; import dev.railroadide.railroad.Services; import dev.railroadide.railroad.project.*; import dev.railroadide.railroad.project.creation.ui.ProjectCreationPane; +import dev.railroadide.railroad.project.data.FabricProjectKeys; import dev.railroadide.railroad.project.data.ForgeProjectKeys; import dev.railroadide.railroad.project.data.MavenProjectKeys; import dev.railroadide.railroad.project.data.MinecraftProjectKeys; @@ -25,6 +27,8 @@ import dev.railroadide.railroad.settings.Settings; import dev.railroadide.railroad.settings.handler.SettingsHandler; import dev.railroadide.railroad.switchboard.SwitchboardRepositories; +import dev.railroadide.railroad.switchboard.repositories.FabricApiVersionRepository; +import dev.railroadide.railroad.switchboard.repositories.NeoforgeVersionRepository; import dev.railroadide.railroad.utility.ExpiringCache; import dev.railroadide.railroad.welcome.project.ui.widget.StarableListCell; import javafx.application.Platform; @@ -51,33 +55,22 @@ // TODO: Make it so the display test and client side only options are only shown for versions that support it // TODO: Make it so the display test and client side only options are in their own steps // TODO: Fix the comboboxes not being immediately populated and instead having the data fetched completely async -public class NeoforgeProjectOnboarding { - private static final ExpiringCache> NEOFORGE_MINECRAFT_VERSIONS_CACHE = new ExpiringCache<>(Duration.ofHours(3)); - +public class NeoforgeProjectOnboarding extends Onboarding { private final ExecutorService executor = Executors.newFixedThreadPool(4); public void start(Scene scene) { var flow = OnboardingFlow.builder() - .addStep("project_details", this::createProjectDetailsStep) - .addStep("maven_coordinates", this::createMavenCoordinatesStep) + .addStep("project_details", this::createProjectDetailsStep) // name, loc + .addStep("maven_coordinates", this::createMavenCoordinatesStep) // groupid, artifact id, version .addStep("minecraft_version", this::createMinecraftVersionStep) - .addStep("neoforge_version", this::createNeoforgeVersionStep) - .addStep("mapping_channel", this::createMappingChannelStep) + .addStep("mapping_channel", this::createMappingChannelStep) // parchment or mojmaps .addStep("mapping_version", this::createMappingVersionStep) - .addStep("mod_details", this::createModDetailsStep) + .addStep("neo", this::createNeoStep) // neoforge version + .addStep("mod_details", this::createModDetailsStep) // id, name, class .addStep("license", this::createLicenseStep) - .addStep("git", this::createGitStep) - .addStep("optional_details", this::createOptionalDetailsStep) + .addStep("git", this::createGitStep) // make git repo + .addStep("optional_details", this::createOptionalDetailsStep) // author, desc, website, sources .firstStep("project_details") - .addTransition("project_details", "maven_coordinates") - .addTransition("maven_coordinates", "minecraft_version") - .addTransition("minecraft_version", "neoforge_version") - .addTransition("neoforge_version", "mapping_channel") - .addTransition("mapping_channel", "mapping_version") - .addTransition("mapping_version", "mod_details") - .addTransition("mod_details", "license") - .addTransition("license", "git") - .addTransition("git", "optional_details") .build(); var process = OnboardingProcess.createBasic( @@ -89,77 +82,43 @@ public void start(Scene scene) { process.run(scene); } - private void onFinish(OnboardingContext ctx, Scene scene) { - executor.shutdown(); + @Override + protected void onFinish(OnboardingContext ctx, Scene scene) { + this.executor.shutdown(); var data = new ProjectData(); data.set(ProjectData.DefaultKeys.TYPE, ProjectTypeRegistry.NEOFORGE); data.set(ProjectData.DefaultKeys.NAME, ctx.get(ProjectData.DefaultKeys.NAME)); data.set(ProjectData.DefaultKeys.PATH, ctx.get(ProjectData.DefaultKeys.PATH)); - data.set(ProjectData.DefaultKeys.INIT_GIT, Boolean.TRUE.equals(ctx.get(ProjectData.DefaultKeys.INIT_GIT))); - data.set(ProjectData.DefaultKeys.LICENSE, ctx.get(ProjectData.DefaultKeys.LICENSE)); + data.set(ProjectData.DefaultKeys.INIT_GIT, ctx.get(ProjectData.DefaultKeys.INIT_GIT)); - if (ctx.contains(ProjectData.DefaultKeys.LICENSE_CUSTOM)) { + data.set(ProjectData.DefaultKeys.LICENSE, ctx.get(ProjectData.DefaultKeys.LICENSE)); + // TODO: Get rid of this and move into CustomLicense + if (ctx.contains(ProjectData.DefaultKeys.LICENSE_CUSTOM)) data.set(ProjectData.DefaultKeys.LICENSE_CUSTOM, ctx.get(ProjectData.DefaultKeys.LICENSE_CUSTOM)); - } data.set(MinecraftProjectKeys.MINECRAFT_VERSION, ctx.get(MinecraftProjectKeys.MINECRAFT_VERSION)); - data.set(ForgeProjectKeys.FORGE_VERSION, ctx.get(ForgeProjectKeys.FORGE_VERSION)); - data.set(MinecraftProjectKeys.MAPPING_CHANNEL, ctx.get(MinecraftProjectKeys.MAPPING_CHANNEL)); - data.set(MinecraftProjectKeys.MAPPING_VERSION, ctx.get(MinecraftProjectKeys.MAPPING_VERSION)); + + if (ctx.contains(ForgeProjectKeys.FORGE_VERSION)) + data.set(ForgeProjectKeys.FORGE_VERSION, ctx.get(ForgeProjectKeys.FORGE_VERSION)); + data.set(MinecraftProjectKeys.MOD_ID, ctx.get(MinecraftProjectKeys.MOD_ID)); data.set(MinecraftProjectKeys.MOD_NAME, ctx.get(MinecraftProjectKeys.MOD_NAME)); data.set(MinecraftProjectKeys.MAIN_CLASS, ctx.get(MinecraftProjectKeys.MAIN_CLASS)); - data.set(ForgeProjectKeys.USE_MIXINS, Boolean.TRUE.equals(ctx.get(ForgeProjectKeys.USE_MIXINS))); - data.set(ForgeProjectKeys.USE_ACCESS_TRANSFORMER, Boolean.TRUE.equals(ctx.get(ForgeProjectKeys.USE_ACCESS_TRANSFORMER))); - data.set(ForgeProjectKeys.GEN_RUN_FOLDERS, Boolean.TRUE.equals(ctx.get(ForgeProjectKeys.GEN_RUN_FOLDERS))); - - if (ctx.contains(ProjectData.DefaultKeys.AUTHOR)) { - String author = ctx.get(ProjectData.DefaultKeys.AUTHOR); - if (!isNullOrBlank(author)) { - data.set(ProjectData.DefaultKeys.AUTHOR, author); - } - } - - if (ctx.contains(ProjectData.DefaultKeys.CREDITS)) { - String credits = ctx.get(ProjectData.DefaultKeys.CREDITS); - if (!isNullOrBlank(credits)) { - data.set(ProjectData.DefaultKeys.CREDITS, credits); - } - } - - if (ctx.contains(ProjectData.DefaultKeys.DESCRIPTION)) { - String description = ctx.get(ProjectData.DefaultKeys.DESCRIPTION); - if (!isNullOrBlank(description)) { - data.set(ProjectData.DefaultKeys.DESCRIPTION, description); - } - } - - if (ctx.contains(ProjectData.DefaultKeys.ISSUES_URL)) { - String issuesUrl = ctx.get(ProjectData.DefaultKeys.ISSUES_URL); - if (!isNullOrBlank(issuesUrl)) { - data.set(ProjectData.DefaultKeys.ISSUES_URL, issuesUrl); - } - } - - if (ctx.contains(ForgeProjectKeys.UPDATE_JSON_URL)) { - String updateJson = ctx.get(ForgeProjectKeys.UPDATE_JSON_URL); - if (!isNullOrBlank(updateJson)) { - data.set(ForgeProjectKeys.UPDATE_JSON_URL, updateJson); - } - } - - if (ctx.contains(ForgeProjectKeys.DISPLAY_URL)) { - String displayUrl = ctx.get(ForgeProjectKeys.DISPLAY_URL); - if (!isNullOrBlank(displayUrl)) { - data.set(ForgeProjectKeys.DISPLAY_URL, displayUrl); - } - } + data.set(ForgeProjectKeys.CLIENT_SIDE_ONLY, ctx.get(ForgeProjectKeys.CLIENT_SIDE_ONLY)); + data.set(MinecraftProjectKeys.MAPPING_CHANNEL, ctx.get(MinecraftProjectKeys.MAPPING_CHANNEL)); + data.set(MinecraftProjectKeys.MAPPING_VERSION, ctx.get(MinecraftProjectKeys.MAPPING_VERSION)); - DisplayTest displayTest = Optional.ofNullable((DisplayTest) ctx.get(ForgeProjectKeys.DISPLAY_TEST)) - .orElse(DisplayTest.MATCH_VERSION); - data.set(ForgeProjectKeys.DISPLAY_TEST, displayTest); - data.set(ForgeProjectKeys.CLIENT_SIDE_ONLY, Boolean.TRUE.equals(ctx.get(ForgeProjectKeys.CLIENT_SIDE_ONLY))); + if (ctx.contains(ProjectData.DefaultKeys.AUTHOR)) + data.set(ProjectData.DefaultKeys.AUTHOR, ctx.get(ProjectData.DefaultKeys.AUTHOR)); + if (ctx.contains(ProjectData.DefaultKeys.DESCRIPTION)) + data.set(ProjectData.DefaultKeys.DESCRIPTION, ctx.get(ProjectData.DefaultKeys.DESCRIPTION)); + if (ctx.contains(ProjectData.DefaultKeys.ISSUES_URL)) + data.set(ProjectData.DefaultKeys.ISSUES_URL, ctx.get(ProjectData.DefaultKeys.ISSUES_URL)); + if (ctx.contains(ProjectData.DefaultKeys.HOMEPAGE_URL)) + data.set(ProjectData.DefaultKeys.HOMEPAGE_URL, ctx.get(ProjectData.DefaultKeys.HOMEPAGE_URL)); + if (ctx.contains(ProjectData.DefaultKeys.SOURCES_URL)) + data.set(ProjectData.DefaultKeys.SOURCES_URL, ctx.get(ProjectData.DefaultKeys.SOURCES_URL)); data.set(MavenProjectKeys.GROUP_ID, ctx.get(MavenProjectKeys.GROUP_ID)); data.set(MavenProjectKeys.ARTIFACT_ID, ctx.get(MavenProjectKeys.ARTIFACT_ID)); @@ -174,188 +133,16 @@ private void onFinish(OnboardingContext ctx, Scene scene) { serviceRegistry ), creationPane.getContext())); - scene.setRoot(creationPane); - } - - private OnboardingStep createProjectDetailsStep() { - return OnboardingFormStep.builder() - .id("project_details") - .title("railroad.project.creation.project_details.title") - .description("railroad.project.creation.project_details.description") - .appendSection("railroad.project.creation.section.project", - described( - FormComponent.textField(ProjectData.DefaultKeys.NAME, "railroad.project.creation.name") - .required() - .promptText("railroad.project.creation.name.prompt") - .validator(ProjectValidators::validateProjectName), - "railroad.project.creation.name.info"), - described( - FormComponent.directoryChooser(ProjectData.DefaultKeys.PATH, "railroad.project.creation.location") - .required() - .defaultPath(System.getProperty("user.home")) - .validator(ProjectValidators::validatePath), - value -> { - if (value == null) - return null; - - String text = value.toString(); - return text.isBlank() ? null : Path.of(text); - }, - value -> { - if (value == null) - return null; - - return value instanceof Path path ? path.toAbsolutePath().toString() : value.toString(); - }, - "railroad.project.creation.location.info")) - .build(); - } - - private OnboardingStep createMavenCoordinatesStep() { - StringProperty artifactId = new SimpleStringProperty(); - String configuredGroupId = SettingsHandler.getValue(Settings.DEFAULT_PROJECT_GROUP_ID); - String configuredVersion = SettingsHandler.getValue(Settings.DEFAULT_PROJECT_VERSION); - String defaultGroupId = isNullOrBlank(configuredGroupId) ? "" : configuredGroupId; - String defaultVersion = isNullOrBlank(configuredVersion) ? "1.0.0" : configuredVersion; - - return OnboardingFormStep.builder() - .id("maven_coordinates") - .title("railroad.project.creation.maven_coordinates.title") - .description("railroad.project.creation.maven_coordinates.description") - .appendSection("railroad.project.creation.section.maven_coordinates", - described( - FormComponent.textField(MavenProjectKeys.GROUP_ID, "railroad.project.creation.group_id") - .required() - .promptText("railroad.project.creation.group_id.prompt") - .text(() -> defaultGroupId) - .validator(ProjectValidators::validateGroupId), - "railroad.project.creation.group_id.info"), - described( - FormComponent.textField(MavenProjectKeys.ARTIFACT_ID, "railroad.project.creation.artifact_id") - .required() - .promptText("railroad.project.creation.artifact_id.prompt") - .text(artifactId::get) - .validator(ProjectValidators::validateArtifactId), - "railroad.project.creation.artifact_id.info"), - described( - FormComponent.textField(MavenProjectKeys.VERSION, "railroad.project.creation.version") - .required() - .promptText("railroad.project.creation.version.prompt") - .text(() -> defaultVersion) - .validator(ProjectValidators::validateVersion), - "railroad.project.creation.version.info")) - .onEnter(ctx -> { - String projectName = ctx.get(ProjectData.DefaultKeys.NAME); - if (projectName != null) { - String defaultArtifactId = ProjectValidators.projectNameToArtifactId(projectName); - if (isNullOrBlank(artifactId.get())) { - artifactId.set(defaultArtifactId); - } - } - }) - .build(); - } - - private OnboardingStep createMinecraftVersionStep() { - ObservableList availableVersions = FXCollections.observableArrayList(); - AtomicLong nextInvalidationTime = new AtomicLong(0L); + data.set(ForgeProjectKeys.USE_MIXINS, true); + data.set(ForgeProjectKeys.USE_ACCESS_TRANSFORMER, true); + data.set(ForgeProjectKeys.DISPLAY_TEST, true); + data.set(ForgeProjectKeys.GEN_RUN_FOLDERS, true); - return OnboardingFormStep.builder() - .id("minecraft_version") - .title("railroad.project.creation.minecraft_version.title") - .description("railroad.project.creation.minecraft_version.description") - .appendSection("railroad.project.creation.section.minecraft_version", - described( - FormComponent.comboBox(MinecraftProjectKeys.MINECRAFT_VERSION, "railroad.project.creation.minecraft_version", MinecraftVersion.class) - .items(() -> availableVersions) - .defaultValue(() -> determineDefaultMinecraftVersion(availableVersions)) - .keyFunction(MinecraftVersion::id) - .valueOfFunction(NeoforgeProjectOnboarding::getMinecraftVersion) - .required() - .translate(false), - "railroad.project.creation.minecraft_version.info")) - .onEnter(ctx -> { - long now = System.currentTimeMillis(); - if (availableVersions.isEmpty() || now > nextInvalidationTime.get()) { - NEOFORGE_MINECRAFT_VERSIONS_CACHE.getIfPresent().ifPresent(values -> - Platform.runLater(() -> { - availableVersions.setAll(values); - ctx.markForRefresh(MinecraftProjectKeys.MINECRAFT_VERSION); - })); - - resolveNeoforgeMinecraftVersions().whenComplete((versions, throwable) -> { - if (throwable != null) { - Railroad.LOGGER.error("Failed to fetch Minecraft versions for Neoforge", throwable); - return; - } - - Platform.runLater(() -> { - availableVersions.setAll(versions); - ctx.markForRefresh(MinecraftProjectKeys.MINECRAFT_VERSION); - }); - }); - - nextInvalidationTime.set(now + TimeUnit.MINUTES.toMillis(5)); - } - }) - .build(); - } - - private OnboardingStep createNeoforgeVersionStep() { - ObservableList availableVersions = FXCollections.observableArrayList(); - StringProperty latestNeoforgeVersion = new SimpleStringProperty(); - - return OnboardingFormStep.builder() - .id("neoforge_version") - .title("railroad.project.creation.neoforge_version.title") - .description("railroad.project.creation.neoforge_version.description") - .appendSection("railroad.project.creation.section.neoforge_version", - described( - FormComponent.comboBox(ForgeProjectKeys.FORGE_VERSION, "railroad.project.creation.neoforge_version", String.class) - .required() - .items(() -> availableVersions) - .defaultValue(() -> { - String latest = latestNeoforgeVersion.get(); - if (latest != null && availableVersions.contains(latest)) - return latest; - - if (!availableVersions.isEmpty()) - return availableVersions.getFirst(); - - return null; - }) - .translate(false) - .cellFactory(param -> new StarableListCell<>( - version -> isRecommendedNeoforgeVersion(version, latestNeoforgeVersion.get()), - latest -> Objects.equals(latest, latestNeoforgeVersion.get()), - Function.identity())) - .buttonCell(new StarableListCell<>( - version -> isRecommendedNeoforgeVersion(version, latestNeoforgeVersion.get()), - latest -> Objects.equals(latest, latestNeoforgeVersion.get()), - Function.identity())), - "railroad.project.creation.neoforge_version.info")) - .onEnter(ctx -> { - MinecraftVersion minecraftVersion = ctx.get(MinecraftProjectKeys.MINECRAFT_VERSION); - - fetchNeoforgeVersions(minecraftVersion).whenComplete((payload, throwable) -> { - if (throwable != null) { - Railroad.LOGGER.error("Failed to fetch Neoforge versions for Minecraft {}", minecraftVersion, throwable); - return; - } - - Platform.runLater(() -> { - availableVersions.setAll(payload.versions()); - latestNeoforgeVersion.set(payload.latest()); - ctx.markForRefresh(ForgeProjectKeys.FORGE_VERSION); - }); - }); - }) - .build(); + scene.setRoot(creationPane); } private OnboardingStep createMappingChannelStep() { ObservableList availableChannels = FXCollections.observableArrayList(); - return OnboardingFormStep.builder() .id("mapping_channel") .title("railroad.project.creation.mapping_channel.title") @@ -365,28 +152,18 @@ private OnboardingStep createMappingChannelStep() { FormComponent.comboBox(MinecraftProjectKeys.MAPPING_CHANNEL, "railroad.project.creation.mapping_channel", MappingChannel.class) .required() .items(() -> availableChannels) - .defaultValue(() -> { - if (availableChannels.contains(MappingChannelRegistry.MOJMAP)) - return MappingChannelRegistry.MOJMAP; - - if (!availableChannels.isEmpty()) - return availableChannels.getFirst(); - - return null; - }) + .defaultValue(() -> MappingChannelRegistry.PARCHMENT) .keyFunction(MappingChannel::id) .valueOfFunction(MappingChannel.REGISTRY::get) .defaultDisplayNameFunction(MappingChannel::translationKey) .translate(true), "railroad.project.creation.mapping_channel.info")) .onEnter(ctx -> { - MinecraftVersion minecraftVersion = ctx.get(MinecraftProjectKeys.MINECRAFT_VERSION); - if (minecraftVersion != null) { - List channels = MappingChannelRegistry.findValidMappingChannels(minecraftVersion); - Platform.runLater(() -> { - availableChannels.setAll(channels); - ctx.markForRefresh(MinecraftProjectKeys.MAPPING_CHANNEL); - }); + MinecraftVersion mcVersion = ctx.get(MinecraftProjectKeys.MINECRAFT_VERSION); + if (mcVersion != null) { + availableChannels.clear(); + availableChannels.setAll(MappingChannelRegistry.findValidMappingChannels(mcVersion)); + ctx.markForRefresh(MinecraftProjectKeys.MAPPING_CHANNEL); } }) .build(); @@ -394,7 +171,6 @@ private OnboardingStep createMappingChannelStep() { private OnboardingStep createMappingVersionStep() { ObservableList availableVersions = FXCollections.observableArrayList(); - return OnboardingFormStep.builder() .id("mapping_version") .title("railroad.project.creation.mapping_version.title") @@ -406,400 +182,93 @@ private OnboardingStep createMappingVersionStep() { .items(() -> availableVersions) .defaultValue(() -> { if (!availableVersions.isEmpty()) - return availableVersions.getLast(); + return availableVersions.getFirst(); return null; }) .translate(false), "railroad.project.creation.mapping_version.info")) .onEnter(ctx -> { - MinecraftVersion minecraftVersion = ctx.get(MinecraftProjectKeys.MINECRAFT_VERSION); - MappingChannel mappingChannel = ctx.get(MinecraftProjectKeys.MAPPING_CHANNEL); - if (minecraftVersion != null && mappingChannel != null) { - List versions = mappingChannel.listVersionsFor(minecraftVersion); - Platform.runLater(() -> { - availableVersions.setAll(versions); - ctx.markForRefresh(MinecraftProjectKeys.MAPPING_VERSION); - }); - } - }) - .build(); - } - - private OnboardingStep createModDetailsStep() { - StringProperty modIdProperty = new SimpleStringProperty(); - StringProperty modNameProperty = new SimpleStringProperty(); - StringProperty mainClassProperty = new SimpleStringProperty(); - - ObjectProperty modIdField = new SimpleObjectProperty<>(); - ObjectProperty modNameField = new SimpleObjectProperty<>(); - ObjectProperty mainClassField = new SimpleObjectProperty<>(); - - bindTextField(modIdProperty, modIdField); - bindTextField(modNameProperty, modNameField); - bindTextField(mainClassProperty, mainClassField); - - return OnboardingFormStep.builder() - .id("mod_details") - .title("railroad.project.creation.mod_details.title") - .description("railroad.project.creation.mod_details.description") - .appendSection("railroad.project.creation.section.mod_details", - described( - FormComponent.textField(MinecraftProjectKeys.MOD_ID, "railroad.project.creation.mod_id") - .required() - .promptText("railroad.project.creation.mod_id.prompt") - .text(modIdProperty::get) - .bindTextFieldTo(modIdField) - .validator(ProjectValidators::validateModId), - "railroad.project.creation.mod_id.info"), - described( - FormComponent.textField(MinecraftProjectKeys.MOD_NAME, "railroad.project.creation.mod_name") - .required() - .promptText("railroad.project.creation.mod_name.prompt") - .text(modNameProperty::get) - .bindTextFieldTo(modNameField) - .validator(ProjectValidators::validateModName), - "railroad.project.creation.mod_name.info"), - described( - FormComponent.textField(MinecraftProjectKeys.MAIN_CLASS, "railroad.project.creation.main_class") - .required() - .promptText("railroad.project.creation.main_class.prompt") - .text(mainClassProperty::get) - .bindTextFieldTo(mainClassField) - .validator(ProjectValidators::validateMainClass), - "railroad.project.creation.main_class.info")) - .appendSection("railroad.project.creation.section.neoforge_options", - described( - FormComponent.checkBox(ForgeProjectKeys.USE_MIXINS, "railroad.project.creation.use_mixins"), - "railroad.project.creation.use_mixins.info"), - described( - FormComponent.checkBox(ForgeProjectKeys.USE_ACCESS_TRANSFORMER, "railroad.project.creation.use_access_transformer"), - "railroad.project.creation.use_access_transformer.info"), - described( - FormComponent.checkBox(ForgeProjectKeys.GEN_RUN_FOLDERS, "railroad.project.creation.gen_run_folders"), - "railroad.project.creation.gen_run_folders.info")) - .onEnter(ctx -> { - String projectName = ctx.get(ProjectData.DefaultKeys.NAME); - - if (!isNullOrBlank(projectName)) { - modIdProperty.set(ProjectValidators.projectNameToModId(projectName)); - } - - if (!isNullOrBlank(projectName)) { - modNameProperty.set(projectName); - } - - if (!isNullOrBlank(projectName)) { - String mainClassName = ProjectValidators.projectNameToMainClass(projectName); - mainClassProperty.set(isNullOrBlank(mainClassName) ? "" : mainClassName); + MinecraftVersion mcVersion = ctx.get(MinecraftProjectKeys.MINECRAFT_VERSION); + MappingChannel channel = ctx.get(MinecraftProjectKeys.MAPPING_CHANNEL); + if (mcVersion != null && channel != null) { + List newVersions = channel.listVersionsFor(mcVersion); + availableVersions.clear(); + availableVersions.setAll(newVersions); + ctx.markForRefresh(MinecraftProjectKeys.MAPPING_VERSION); } }) .build(); } - private OnboardingStep createLicenseStep() { - ObservableList availableLicenses = FXCollections.observableArrayList(); - ObjectProperty> licenseComboBox = new SimpleObjectProperty<>(); - BooleanProperty showCustomLicense = new SimpleBooleanProperty(false); - ChangeListener licenseSelectionListener = (observable, oldValue, newValue) -> - showCustomLicense.set(newValue == LicenseRegistry.CUSTOM); - - licenseComboBox.addListener((observable, oldValue, newValue) -> { - if (oldValue != null) { - oldValue.valueProperty().removeListener(licenseSelectionListener); - } - - if (newValue != null) { - showCustomLicense.set(newValue.getValue() == LicenseRegistry.CUSTOM); - newValue.valueProperty().addListener(licenseSelectionListener); - } else { - showCustomLicense.set(false); - } - }); - - BooleanBinding customLicenseVisible = Bindings.createBooleanBinding(showCustomLicense::get, showCustomLicense); - + private OnboardingStep createNeoStep() { + ObservableList availableVersions = FXCollections.observableArrayList(); return OnboardingFormStep.builder() - .id("license") - .title("railroad.project.creation.license.title") - .description("railroad.project.creation.license.description") - .appendSection("railroad.project.creation.section.license", + .id("neo") + .title("railroad.project.creation.neo.title") + .description("railroad.project.creation.neo.description") + .appendSection("railroad.project.creation.section.neo", described( - FormComponent.comboBox(ProjectData.DefaultKeys.LICENSE, "railroad.project.creation.license", License.class) - .required() - .bindComboBoxTo(licenseComboBox) - .keyFunction(License::getSpdxId) - .valueOfFunction(License::fromSpdxId) - .defaultDisplayNameFunction(License::getName) - .translate(false) - .items(() -> availableLicenses) + FormComponent.comboBox(ForgeProjectKeys.FORGE_VERSION, "railroad.project.creation.neo", String.class) + .items(() -> availableVersions) .defaultValue(() -> { - if (availableLicenses.contains(LicenseRegistry.LGPL)) - return LicenseRegistry.LGPL; - - if (!availableLicenses.isEmpty()) - return availableLicenses.getFirst(); + if (!availableVersions.isEmpty()) + return availableVersions.getFirst(); return null; - }), - "railroad.project.creation.license.info"), - described( - FormComponent.textField(ProjectData.DefaultKeys.LICENSE_CUSTOM, "railroad.project.creation.license.custom") - .visible(customLicenseVisible) - .promptText("railroad.project.creation.license.custom.prompt") - .validator(ProjectValidators::validateCustomLicense), - "railroad.project.creation.license.custom.info")) + }) + .translate(false), + "railroad.project.creation.neo.info")) .onEnter(ctx -> { - List newValues = License.REGISTRY.values() - .stream() - .sorted(Comparator.comparing(License::getName)) - .toList(); - - if (availableLicenses.size() != newValues.size() || !ListUtils.isEqualList(availableLicenses, newValues)) { - availableLicenses.clear(); - availableLicenses.addAll(newValues); - ctx.markForRefresh(ProjectData.DefaultKeys.LICENSE); + MinecraftVersion mcVersion = ctx.get(MinecraftProjectKeys.MINECRAFT_VERSION); + if (mcVersion != null) { + try { + CompletableFuture> versionsFuture = SwitchboardRepositories.NEOFORGE.getVersionsFor(mcVersion.id()); + List versions = versionsFuture.get(); + availableVersions.clear(); + availableVersions.addAll(versions); + ctx.markForRefresh(ForgeProjectKeys.FORGE_VERSION); + } catch (ExecutionException | InterruptedException exception) { + Railroad.LOGGER.error("Failed to fetch Neoforge versions for Minecraft {}", mcVersion.id(), exception); + } } }) .build(); } - private OnboardingStep createGitStep() { - return OnboardingFormStep.builder() - .id("git") - .title("railroad.project.creation.git.title") - .description("railroad.project.creation.git.description") - .appendSection("railroad.project.creation.section.git", - described( - FormComponent.checkBox(ProjectData.DefaultKeys.INIT_GIT, "railroad.project.creation.init_git") - .selected(true), - "railroad.project.creation.init_git.info")) - .build(); - } - - private OnboardingStep createOptionalDetailsStep() { - String configuredAuthor = SettingsHandler.getValue(Settings.DEFAULT_PROJECT_AUTHOR); - String defaultAuthor = !isNullOrBlank(configuredAuthor) - ? configuredAuthor - : Optional.ofNullable(System.getProperty("user.name")) - .filter(name -> !isNullOrBlank(name)) - .orElse(""); - - return OnboardingFormStep.builder() - .id("optional_details") - .title("railroad.project.creation.optional_details.title") - .description("railroad.project.creation.optional_details.description") - .appendSection("railroad.project.creation.section.optional_details", - described( - FormComponent.textField(ProjectData.DefaultKeys.AUTHOR, "railroad.project.creation.author") - .text(() -> defaultAuthor) - .promptText("railroad.project.creation.author.prompt") - .validator(ProjectValidators::validateAuthor), - "railroad.project.creation.author.info"), - described( - FormComponent.textField(ProjectData.DefaultKeys.CREDITS, "railroad.project.creation.credits") - .promptText("railroad.project.creation.credits.prompt") - .validator(ProjectValidators::validateCredits), - "railroad.project.creation.credits.info"), - described( - FormComponent.textArea(ProjectData.DefaultKeys.DESCRIPTION, "railroad.project.creation.description") - .promptText("railroad.project.creation.description.prompt") - .validator(ProjectValidators::validateDescription), - "railroad.project.creation.description.info"), - described( - FormComponent.textField(ProjectData.DefaultKeys.ISSUES_URL, "railroad.project.creation.issues_url") - .promptText("railroad.project.creation.issues_url.prompt") - .validator(ProjectValidators::validateIssues), - "railroad.project.creation.issues_url.info"), - described( - FormComponent.textField(ForgeProjectKeys.UPDATE_JSON_URL, "railroad.project.creation.update_json_url") - .promptText("railroad.project.creation.update_json_url.prompt") - .validator(ProjectValidators::validateUpdateJsonUrl), - "railroad.project.creation.update_json_url.info"), - described( - FormComponent.textField(ForgeProjectKeys.DISPLAY_URL, "railroad.project.creation.display_url") - .promptText("railroad.project.creation.display_url.prompt") - .validator(field -> ProjectValidators.validateGenericUrl(field, "display_url")), - "railroad.project.creation.display_url.info"), - described( - FormComponent.comboBox(ForgeProjectKeys.DISPLAY_TEST, "railroad.project.creation.display_test", DisplayTest.class) - .items(() -> Arrays.asList(DisplayTest.values())) - .defaultValue(() -> DisplayTest.MATCH_VERSION) - .keyFunction(DisplayTest::name) - .valueOfFunction(DisplayTest::valueOf) - .translate(false), - "railroad.project.creation.display_test.info"), - described( - FormComponent.checkBox(ForgeProjectKeys.CLIENT_SIDE_ONLY, "railroad.project.creation.client_side_only"), - "railroad.project.creation.client_side_only.info")) - .build(); - } - - private static OnboardingFormStep.ComponentSpec described(FormComponentBuilder builder, String descriptionKey) { - return OnboardingFormStep.component(builder, createDescriptionCustomizer(descriptionKey)); - } + @Override + protected List getMinecraftVersions() { - private static OnboardingFormStep.ComponentSpec described(FormComponentBuilder builder, Function transformer, Function reverseTransformer, String descriptionKey) { - return OnboardingFormStep.component(builder, builder != null ? builder.dataKey() : null, transformer, reverseTransformer, createDescriptionCustomizer(descriptionKey)); - } - - private static Consumer> createDescriptionCustomizer(String descriptionKey) { - if (isNullOrBlank(descriptionKey)) - return null; - - return component -> attachDescription(component, descriptionKey); - } - - private static void attachDescription(FormComponent component, String descriptionKey) { - if (component == null || isNullOrBlank(descriptionKey)) - return; - - Consumer applyToNode = node -> { - if (node instanceof InformativeLabeledHBox informative) { - boolean exists = informative.getInformationLabels().stream() - .anyMatch(label -> descriptionKey.equals(label.getKey())); - if (!exists) { - informative.addInformationLabel(descriptionKey); - } - } - }; - - Node currentNode = component.componentProperty().get(); - if (currentNode != null) { - applyToNode.accept(currentNode); - } - - component.componentProperty().addListener((observable, oldValue, newValue) -> { - if (newValue != null) { - applyToNode.accept(newValue); - } - }); - } - - private static MinecraftVersion getMinecraftVersion(String string) { + // TODO: get minecraft versions available for neoforge directly from switchboard rather than version filter try { - return SwitchboardRepositories.MINECRAFT.getVersionSync(string).orElse(null); + return SwitchboardRepositories.MINECRAFT.getAllVersionsSync().stream() + .filter(v -> { + String id = v.id(); + if (!id.matches("\\d+\\.\\d+(\\.\\d+)?")) return false; // exclude non-standard (pre, w, rc, etc.) + String[] p = id.split("\\."); + String[] t = {"1", "20", "4"}; + for (int i = 0; i < Math.max(p.length, t.length); i++) { + int a = i < p.length ? Integer.parseInt(p[i]) : 0; + int b = i < t.length ? Integer.parseInt(t[i]) : 0; + if (a > b) return true; + if (a < b) return false; + } + return true; + }) + .sorted((a, b) -> { + String[] pa = a.id().split("\\."); + String[] pb = b.id().split("\\."); + for (int i = 0; i < Math.max(pa.length, pb.length); i++) { + int va = i < pa.length ? Integer.parseInt(pa[i]) : 0; + int vb = i < pb.length ? Integer.parseInt(pb[i]) : 0; + if (va != vb) return Integer.compare(vb, va); + } + return 0; + }) + .toList(); } catch (ExecutionException | InterruptedException exception) { - Railroad.LOGGER.error("Failed to fetch Minecraft version {}", string, exception); - return null; + Railroad.LOGGER.error("Failed to fetch Minecraft versions", exception); + return Collections.emptyList(); } } - - private static CompletableFuture> resolveNeoforgeMinecraftVersions() { - return NEOFORGE_MINECRAFT_VERSIONS_CACHE.getAsync(() -> - SwitchboardRepositories.NEOFORGE.getAllVersions() - .thenApply(versions -> versions.stream() - .map(NeoforgeProjectOnboarding::extractMinecraftVersionId) - .flatMap(Optional::stream) - .map(NeoforgeProjectOnboarding::lookupMinecraftVersion) - .flatMap(Optional::stream) - .distinct() - .sorted(Comparator.reverseOrder()) - .toList()) - ); - } - - private static CompletableFuture fetchNeoforgeVersions(MinecraftVersion version) { - if (version == null) - return CompletableFuture.completedFuture(new NeoforgeVersionsPayload(null, List.of(), null)); - - String minecraftId = version.id(); - CompletableFuture> versionsFuture = SwitchboardRepositories.NEOFORGE.getVersionsFor(minecraftId); - CompletableFuture latestFuture = SwitchboardRepositories.NEOFORGE.getLatestVersionFor(minecraftId); - - return versionsFuture.thenCombine(latestFuture, (versions, latest) -> - new NeoforgeVersionsPayload(version, versions == null ? List.of() : versions, latest)) - .exceptionally(throwable -> { - Railroad.LOGGER.error("Failed to fetch Neoforge versions for Minecraft {}", version, throwable); - return new NeoforgeVersionsPayload(version, List.of(), null); - }); - } - - private static MinecraftVersion determineDefaultMinecraftVersion(List versions) { - if (versions == null || versions.isEmpty()) - return null; - - return versions.stream() - .filter(version -> version != null && version.getType() == MinecraftVersion.Type.RELEASE) - .findFirst() - .orElseGet(versions::getFirst); - } - - private static Optional lookupMinecraftVersion(String versionId) { - try { - return SwitchboardRepositories.MINECRAFT.getVersionSync(versionId); - } catch (ExecutionException exception) { - Railroad.LOGGER.error("Failed to fetch Minecraft version {}", versionId, exception); - } catch (InterruptedException exception) { - Thread.currentThread().interrupt(); - Railroad.LOGGER.error("Interrupted while fetching Minecraft version {}", versionId, exception); - } - - return Optional.empty(); - } - - private static Optional extractMinecraftVersionId(String neoforgeVersion) { - if (neoforgeVersion == null || neoforgeVersion.isBlank()) - return Optional.empty(); - - String lower = neoforgeVersion.toLowerCase(Locale.ROOT); - if (lower.contains("25w14craftmine")) - return Optional.of("25w14craftmine"); - - int lastDash = neoforgeVersion.indexOf('-'); - if (lastDash <= 0) - return Optional.empty(); - - String base = neoforgeVersion.substring(0, lastDash); - if (base.isBlank()) - return Optional.empty(); - - return Optional.of(base); - } - - private static boolean isRecommendedNeoforgeVersion(String version, String latest) { - return version != null && Objects.equals(version, latest) && !isNeoforgePrerelease(version); - } - - private static boolean isNeoforgePrerelease(String version) { - if (version == null) - return false; - - String lower = version.toLowerCase(Locale.ROOT); - return lower.contains("beta") || lower.contains("alpha") || lower.contains("rc") || lower.contains("25w14craftmine"); - } - - private static boolean isNullOrBlank(String value) { - return value == null || value.isBlank(); - } - - private static void bindTextField(StringProperty valueProperty, ObjectProperty fieldProperty) { - Objects.requireNonNull(valueProperty, "valueProperty"); - Objects.requireNonNull(fieldProperty, "fieldProperty"); - - valueProperty.addListener((obs, oldValue, newValue) -> { - TextField field = fieldProperty.get(); - if (field != null && !Objects.equals(field.getText(), newValue)) { - field.setText(newValue); - } - }); - - fieldProperty.addListener((obs, oldField, newField) -> { - if (newField == null) - return; - - if (!Objects.equals(newField.getText(), valueProperty.get())) { - newField.setText(valueProperty.get()); - } - - newField.textProperty().addListener((textObs, oldText, newText) -> { - if (!Objects.equals(valueProperty.get(), newText)) { - valueProperty.set(newText); - } - }); - }); - } - - private record NeoforgeVersionsPayload(MinecraftVersion contextVersion, List versions, String latest) { - } } diff --git a/src/main/java/dev/railroadide/railroad/project/onboarding/impl/OldNeoforgeProjectOnboarding.java b/src/main/java/dev/railroadide/railroad/project/onboarding/impl/OldNeoforgeProjectOnboarding.java new file mode 100644 index 00000000..9145d892 --- /dev/null +++ b/src/main/java/dev/railroadide/railroad/project/onboarding/impl/OldNeoforgeProjectOnboarding.java @@ -0,0 +1,796 @@ +package dev.railroadide.railroad.project.onboarding.impl; + +import dev.railroadide.core.form.FormComponent; +import dev.railroadide.core.form.FormComponentBuilder; +import dev.railroadide.core.form.ui.InformativeLabeledHBox; +import dev.railroadide.core.project.License; +import dev.railroadide.core.project.ProjectData; +import dev.railroadide.core.project.creation.ProjectCreationService; +import dev.railroadide.core.project.creation.ProjectServiceRegistry; +import dev.railroadide.core.project.creation.service.GradleService; +import dev.railroadide.core.project.minecraft.MappingChannel; +import dev.railroadide.core.switchboard.pojo.MinecraftVersion; +import dev.railroadide.railroad.Railroad; +import dev.railroadide.railroad.Services; +import dev.railroadide.railroad.project.*; +import dev.railroadide.railroad.project.creation.ui.ProjectCreationPane; +import dev.railroadide.railroad.project.data.ForgeProjectKeys; +import dev.railroadide.railroad.project.data.MavenProjectKeys; +import dev.railroadide.railroad.project.data.MinecraftProjectKeys; +import dev.railroadide.railroad.project.onboarding.OnboardingContext; +import dev.railroadide.railroad.project.onboarding.OnboardingProcess; +import dev.railroadide.railroad.project.onboarding.flow.OnboardingFlow; +import dev.railroadide.railroad.project.onboarding.step.OnboardingFormStep; +import dev.railroadide.railroad.project.onboarding.step.OnboardingStep; +import dev.railroadide.railroad.settings.Settings; +import dev.railroadide.railroad.settings.handler.SettingsHandler; +import dev.railroadide.railroad.switchboard.SwitchboardRepositories; +import dev.railroadide.railroad.utility.ExpiringCache; +import dev.railroadide.railroad.welcome.project.ui.widget.StarableListCell; +import javafx.application.Platform; +import javafx.beans.binding.Bindings; +import javafx.beans.binding.BooleanBinding; +import javafx.beans.property.*; +import javafx.beans.value.ChangeListener; +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; +import javafx.scene.Node; +import javafx.scene.Scene; +import javafx.scene.control.ComboBox; +import javafx.scene.control.TextField; +import org.apache.commons.collections.ListUtils; + +import java.nio.file.Path; +import java.time.Duration; +import java.util.*; +import java.util.concurrent.*; +import java.util.concurrent.atomic.AtomicLong; +import java.util.function.Consumer; +import java.util.function.Function; + +// TODO: Make it so the display test and client side only options are only shown for versions that support it +// TODO: Make it so the display test and client side only options are in their own steps +// TODO: Fix the comboboxes not being immediately populated and instead having the data fetched completely async +public class OldNeoforgeProjectOnboarding { + private static final ExpiringCache> NEOFORGE_MINECRAFT_VERSIONS_CACHE = new ExpiringCache<>(Duration.ofHours(3)); + + private final ExecutorService executor = Executors.newFixedThreadPool(4); + + public void start(Scene scene) { + var flow = OnboardingFlow.builder() + .addStep("project_details", this::createProjectDetailsStep) + .addStep("maven_coordinates", this::createMavenCoordinatesStep) + .addStep("minecraft_version", this::createMinecraftVersionStep) + .addStep("neoforge_version", this::createNeoforgeVersionStep) + .addStep("mapping_channel", this::createMappingChannelStep) + .addStep("mapping_version", this::createMappingVersionStep) + .addStep("mod_details", this::createModDetailsStep) + .addStep("license", this::createLicenseStep) + .addStep("git", this::createGitStep) + .addStep("optional_details", this::createOptionalDetailsStep) + .firstStep("project_details") + .build(); + + var process = OnboardingProcess.createBasic( + flow, + new OnboardingContext(executor), + ctx -> onFinish(ctx, scene) + ); + + process.run(scene); + } + + private void onFinish(OnboardingContext ctx, Scene scene) { + executor.shutdown(); + + var data = new ProjectData(); + data.set(ProjectData.DefaultKeys.TYPE, ProjectTypeRegistry.NEOFORGE); + data.set(ProjectData.DefaultKeys.NAME, ctx.get(ProjectData.DefaultKeys.NAME)); + data.set(ProjectData.DefaultKeys.PATH, ctx.get(ProjectData.DefaultKeys.PATH)); + data.set(ProjectData.DefaultKeys.INIT_GIT, Boolean.TRUE.equals(ctx.get(ProjectData.DefaultKeys.INIT_GIT))); + data.set(ProjectData.DefaultKeys.LICENSE, ctx.get(ProjectData.DefaultKeys.LICENSE)); + + if (ctx.contains(ProjectData.DefaultKeys.LICENSE_CUSTOM)) { + data.set(ProjectData.DefaultKeys.LICENSE_CUSTOM, ctx.get(ProjectData.DefaultKeys.LICENSE_CUSTOM)); + } + + data.set(MinecraftProjectKeys.MINECRAFT_VERSION, ctx.get(MinecraftProjectKeys.MINECRAFT_VERSION)); + data.set(ForgeProjectKeys.FORGE_VERSION, ctx.get(ForgeProjectKeys.FORGE_VERSION)); + data.set(MinecraftProjectKeys.MAPPING_CHANNEL, ctx.get(MinecraftProjectKeys.MAPPING_CHANNEL)); + data.set(MinecraftProjectKeys.MAPPING_VERSION, ctx.get(MinecraftProjectKeys.MAPPING_VERSION)); + data.set(MinecraftProjectKeys.MOD_ID, ctx.get(MinecraftProjectKeys.MOD_ID)); + data.set(MinecraftProjectKeys.MOD_NAME, ctx.get(MinecraftProjectKeys.MOD_NAME)); + data.set(MinecraftProjectKeys.MAIN_CLASS, ctx.get(MinecraftProjectKeys.MAIN_CLASS)); + data.set(ForgeProjectKeys.USE_MIXINS, Boolean.TRUE.equals(ctx.get(ForgeProjectKeys.USE_MIXINS))); + data.set(ForgeProjectKeys.USE_ACCESS_TRANSFORMER, Boolean.TRUE.equals(ctx.get(ForgeProjectKeys.USE_ACCESS_TRANSFORMER))); + data.set(ForgeProjectKeys.GEN_RUN_FOLDERS, Boolean.TRUE.equals(ctx.get(ForgeProjectKeys.GEN_RUN_FOLDERS))); + + if (ctx.contains(ProjectData.DefaultKeys.AUTHOR)) { + String author = ctx.get(ProjectData.DefaultKeys.AUTHOR); + if (!isNullOrBlank(author)) { + data.set(ProjectData.DefaultKeys.AUTHOR, author); + } + } + + if (ctx.contains(ProjectData.DefaultKeys.CREDITS)) { + String credits = ctx.get(ProjectData.DefaultKeys.CREDITS); + if (!isNullOrBlank(credits)) { + data.set(ProjectData.DefaultKeys.CREDITS, credits); + } + } + + if (ctx.contains(ProjectData.DefaultKeys.DESCRIPTION)) { + String description = ctx.get(ProjectData.DefaultKeys.DESCRIPTION); + if (!isNullOrBlank(description)) { + data.set(ProjectData.DefaultKeys.DESCRIPTION, description); + } + } + + if (ctx.contains(ProjectData.DefaultKeys.ISSUES_URL)) { + String issuesUrl = ctx.get(ProjectData.DefaultKeys.ISSUES_URL); + if (!isNullOrBlank(issuesUrl)) { + data.set(ProjectData.DefaultKeys.ISSUES_URL, issuesUrl); + } + } + + if (ctx.contains(ForgeProjectKeys.UPDATE_JSON_URL)) { + String updateJson = ctx.get(ForgeProjectKeys.UPDATE_JSON_URL); + if (!isNullOrBlank(updateJson)) { + data.set(ForgeProjectKeys.UPDATE_JSON_URL, updateJson); + } + } + + if (ctx.contains(ForgeProjectKeys.DISPLAY_URL)) { + String displayUrl = ctx.get(ForgeProjectKeys.DISPLAY_URL); + if (!isNullOrBlank(displayUrl)) { + data.set(ForgeProjectKeys.DISPLAY_URL, displayUrl); + } + } + + DisplayTest displayTest = Optional.ofNullable((DisplayTest) ctx.get(ForgeProjectKeys.DISPLAY_TEST)) + .orElse(DisplayTest.MATCH_VERSION); + data.set(ForgeProjectKeys.DISPLAY_TEST, displayTest); + data.set(ForgeProjectKeys.CLIENT_SIDE_ONLY, Boolean.TRUE.equals(ctx.get(ForgeProjectKeys.CLIENT_SIDE_ONLY))); + + data.set(MavenProjectKeys.GROUP_ID, ctx.get(MavenProjectKeys.GROUP_ID)); + data.set(MavenProjectKeys.ARTIFACT_ID, ctx.get(MavenProjectKeys.ARTIFACT_ID)); + data.set(MavenProjectKeys.VERSION, ctx.get(MavenProjectKeys.VERSION)); + + var creationPane = new ProjectCreationPane(data); + + ProjectServiceRegistry serviceRegistry = Services.PROJECT_SERVICE_REGISTRY; + serviceRegistry.get(GradleService.class).setOutputStream(creationPane.getTaos()); + creationPane.initService(new ProjectCreationService(Services.PROJECT_CREATION_PIPELINE.createProject( + ProjectTypeRegistry.NEOFORGE, + serviceRegistry + ), creationPane.getContext())); + + scene.setRoot(creationPane); + } + + private OnboardingStep createProjectDetailsStep() { + return OnboardingFormStep.builder() + .id("project_details") + .title("railroad.project.creation.project_details.title") + .description("railroad.project.creation.project_details.description") + .appendSection("railroad.project.creation.section.project", + described( + FormComponent.textField(ProjectData.DefaultKeys.NAME, "railroad.project.creation.name") + .required() + .promptText("railroad.project.creation.name.prompt") + .validator(ProjectValidators::validateProjectName), + "railroad.project.creation.name.info"), + described( + FormComponent.directoryChooser(ProjectData.DefaultKeys.PATH, "railroad.project.creation.location") + .required() + .defaultPath(System.getProperty("user.home")) + .validator(ProjectValidators::validatePath), + value -> { + if (value == null) + return null; + + String text = value.toString(); + return text.isBlank() ? null : Path.of(text); + }, + value -> { + if (value == null) + return null; + + return value instanceof Path path ? path.toAbsolutePath().toString() : value.toString(); + }, + "railroad.project.creation.location.info")) + .build(); + } + + private OnboardingStep createMavenCoordinatesStep() { + StringProperty artifactId = new SimpleStringProperty(); + String configuredGroupId = SettingsHandler.getValue(Settings.DEFAULT_PROJECT_GROUP_ID); + String configuredVersion = SettingsHandler.getValue(Settings.DEFAULT_PROJECT_VERSION); + String defaultGroupId = isNullOrBlank(configuredGroupId) ? "" : configuredGroupId; + String defaultVersion = isNullOrBlank(configuredVersion) ? "1.0.0" : configuredVersion; + + return OnboardingFormStep.builder() + .id("maven_coordinates") + .title("railroad.project.creation.maven_coordinates.title") + .description("railroad.project.creation.maven_coordinates.description") + .appendSection("railroad.project.creation.section.maven_coordinates", + described( + FormComponent.textField(MavenProjectKeys.GROUP_ID, "railroad.project.creation.group_id") + .required() + .promptText("railroad.project.creation.group_id.prompt") + .text(() -> defaultGroupId) + .validator(ProjectValidators::validateGroupId), + "railroad.project.creation.group_id.info"), + described( + FormComponent.textField(MavenProjectKeys.ARTIFACT_ID, "railroad.project.creation.artifact_id") + .required() + .promptText("railroad.project.creation.artifact_id.prompt") + .text(artifactId::get) + .validator(ProjectValidators::validateArtifactId), + "railroad.project.creation.artifact_id.info"), + described( + FormComponent.textField(MavenProjectKeys.VERSION, "railroad.project.creation.version") + .required() + .promptText("railroad.project.creation.version.prompt") + .text(() -> defaultVersion) + .validator(ProjectValidators::validateVersion), + "railroad.project.creation.version.info")) + .onEnter(ctx -> { + String projectName = ctx.get(ProjectData.DefaultKeys.NAME); + if (projectName != null) { + String defaultArtifactId = ProjectValidators.projectNameToArtifactId(projectName); + if (isNullOrBlank(artifactId.get())) { + artifactId.set(defaultArtifactId); + } + } + }) + .build(); + } + + private OnboardingStep createMinecraftVersionStep() { + ObservableList availableVersions = FXCollections.observableArrayList(); + AtomicLong nextInvalidationTime = new AtomicLong(0L); + + return OnboardingFormStep.builder() + .id("minecraft_version") + .title("railroad.project.creation.minecraft_version.title") + .description("railroad.project.creation.minecraft_version.description") + .appendSection("railroad.project.creation.section.minecraft_version", + described( + FormComponent.comboBox(MinecraftProjectKeys.MINECRAFT_VERSION, "railroad.project.creation.minecraft_version", MinecraftVersion.class) + .items(() -> availableVersions) + .defaultValue(() -> determineDefaultMinecraftVersion(availableVersions)) + .keyFunction(MinecraftVersion::id) + .valueOfFunction(OldNeoforgeProjectOnboarding::getMinecraftVersion) + .required() + .translate(false), + "railroad.project.creation.minecraft_version.info")) + .onEnter(ctx -> { + long now = System.currentTimeMillis(); + if (availableVersions.isEmpty() || now > nextInvalidationTime.get()) { + NEOFORGE_MINECRAFT_VERSIONS_CACHE.getIfPresent().ifPresent(values -> + Platform.runLater(() -> { + availableVersions.setAll(values); + ctx.markForRefresh(MinecraftProjectKeys.MINECRAFT_VERSION); + })); + + resolveNeoforgeMinecraftVersions().whenComplete((versions, throwable) -> { + if (throwable != null) { + Railroad.LOGGER.error("Failed to fetch Minecraft versions for Neoforge", throwable); + return; + } + + Platform.runLater(() -> { + availableVersions.setAll(versions); + ctx.markForRefresh(MinecraftProjectKeys.MINECRAFT_VERSION); + }); + }); + + nextInvalidationTime.set(now + TimeUnit.MINUTES.toMillis(5)); + } + }) + .build(); + } + + private OnboardingStep createNeoforgeVersionStep() { + ObservableList availableVersions = FXCollections.observableArrayList(); + StringProperty latestNeoforgeVersion = new SimpleStringProperty(); + + return OnboardingFormStep.builder() + .id("neoforge_version") + .title("railroad.project.creation.neoforge_version.title") + .description("railroad.project.creation.neoforge_version.description") + .appendSection("railroad.project.creation.section.neoforge_version", + described( + FormComponent.comboBox(ForgeProjectKeys.FORGE_VERSION, "railroad.project.creation.neoforge_version", String.class) + .required() + .items(() -> availableVersions) + .defaultValue(() -> { + String latest = latestNeoforgeVersion.get(); + if (latest != null && availableVersions.contains(latest)) + return latest; + + if (!availableVersions.isEmpty()) + return availableVersions.getFirst(); + + return null; + }) + .translate(false) + .cellFactory(param -> new StarableListCell<>( + version -> isRecommendedNeoforgeVersion(version, latestNeoforgeVersion.get()), + latest -> Objects.equals(latest, latestNeoforgeVersion.get()), + Function.identity())) + .buttonCell(new StarableListCell<>( + version -> isRecommendedNeoforgeVersion(version, latestNeoforgeVersion.get()), + latest -> Objects.equals(latest, latestNeoforgeVersion.get()), + Function.identity())), + "railroad.project.creation.neoforge_version.info")) + .onEnter(ctx -> { + MinecraftVersion minecraftVersion = ctx.get(MinecraftProjectKeys.MINECRAFT_VERSION); + + fetchNeoforgeVersions(minecraftVersion).whenComplete((payload, throwable) -> { + if (throwable != null) { + Railroad.LOGGER.error("Failed to fetch Neoforge versions for Minecraft {}", minecraftVersion, throwable); + return; + } + + Platform.runLater(() -> { + availableVersions.setAll(payload.versions()); + latestNeoforgeVersion.set(payload.latest()); + ctx.markForRefresh(ForgeProjectKeys.FORGE_VERSION); + }); + }); + }) + .build(); + } + + private OnboardingStep createMappingChannelStep() { + ObservableList availableChannels = FXCollections.observableArrayList(); + + return OnboardingFormStep.builder() + .id("mapping_channel") + .title("railroad.project.creation.mapping_channel.title") + .description("railroad.project.creation.mapping_channel.description") + .appendSection("railroad.project.creation.section.mapping_channel", + described( + FormComponent.comboBox(MinecraftProjectKeys.MAPPING_CHANNEL, "railroad.project.creation.mapping_channel", MappingChannel.class) + .required() + .items(() -> availableChannels) + .defaultValue(() -> { + if (availableChannels.contains(MappingChannelRegistry.MOJMAP)) + return MappingChannelRegistry.MOJMAP; + + if (!availableChannels.isEmpty()) + return availableChannels.getFirst(); + + return null; + }) + .keyFunction(MappingChannel::id) + .valueOfFunction(MappingChannel.REGISTRY::get) + .defaultDisplayNameFunction(MappingChannel::translationKey) + .translate(true), + "railroad.project.creation.mapping_channel.info")) + .onEnter(ctx -> { + MinecraftVersion minecraftVersion = ctx.get(MinecraftProjectKeys.MINECRAFT_VERSION); + if (minecraftVersion != null) { + List channels = MappingChannelRegistry.findValidMappingChannels(minecraftVersion); + Platform.runLater(() -> { + availableChannels.setAll(channels); + ctx.markForRefresh(MinecraftProjectKeys.MAPPING_CHANNEL); + }); + } + }) + .build(); + } + + private OnboardingStep createMappingVersionStep() { + ObservableList availableVersions = FXCollections.observableArrayList(); + + return OnboardingFormStep.builder() + .id("mapping_version") + .title("railroad.project.creation.mapping_version.title") + .description("railroad.project.creation.mapping_version.description") + .appendSection("railroad.project.creation.section.mapping_version", + described( + FormComponent.comboBox(MinecraftProjectKeys.MAPPING_VERSION, "railroad.project.creation.mapping_version", String.class) + .required() + .items(() -> availableVersions) + .defaultValue(() -> { + if (!availableVersions.isEmpty()) + return availableVersions.getLast(); + + return null; + }) + .translate(false), + "railroad.project.creation.mapping_version.info")) + .onEnter(ctx -> { + MinecraftVersion minecraftVersion = ctx.get(MinecraftProjectKeys.MINECRAFT_VERSION); + MappingChannel mappingChannel = ctx.get(MinecraftProjectKeys.MAPPING_CHANNEL); + if (minecraftVersion != null && mappingChannel != null) { + List versions = mappingChannel.listVersionsFor(minecraftVersion); + Platform.runLater(() -> { + availableVersions.setAll(versions); + ctx.markForRefresh(MinecraftProjectKeys.MAPPING_VERSION); + }); + } + }) + .build(); + } + + private OnboardingStep createModDetailsStep() { + StringProperty modIdProperty = new SimpleStringProperty(); + StringProperty modNameProperty = new SimpleStringProperty(); + StringProperty mainClassProperty = new SimpleStringProperty(); + + ObjectProperty modIdField = new SimpleObjectProperty<>(); + ObjectProperty modNameField = new SimpleObjectProperty<>(); + ObjectProperty mainClassField = new SimpleObjectProperty<>(); + + bindTextField(modIdProperty, modIdField); + bindTextField(modNameProperty, modNameField); + bindTextField(mainClassProperty, mainClassField); + + return OnboardingFormStep.builder() + .id("mod_details") + .title("railroad.project.creation.mod_details.title") + .description("railroad.project.creation.mod_details.description") + .appendSection("railroad.project.creation.section.mod_details", + described( + FormComponent.textField(MinecraftProjectKeys.MOD_ID, "railroad.project.creation.mod_id") + .required() + .promptText("railroad.project.creation.mod_id.prompt") + .text(modIdProperty::get) + .bindTextFieldTo(modIdField) + .validator(ProjectValidators::validateModId), + "railroad.project.creation.mod_id.info"), + described( + FormComponent.textField(MinecraftProjectKeys.MOD_NAME, "railroad.project.creation.mod_name") + .required() + .promptText("railroad.project.creation.mod_name.prompt") + .text(modNameProperty::get) + .bindTextFieldTo(modNameField) + .validator(ProjectValidators::validateModName), + "railroad.project.creation.mod_name.info"), + described( + FormComponent.textField(MinecraftProjectKeys.MAIN_CLASS, "railroad.project.creation.main_class") + .required() + .promptText("railroad.project.creation.main_class.prompt") + .text(mainClassProperty::get) + .bindTextFieldTo(mainClassField) + .validator(ProjectValidators::validateMainClass), + "railroad.project.creation.main_class.info")) + .appendSection("railroad.project.creation.section.neoforge_options", + described( + FormComponent.checkBox(ForgeProjectKeys.USE_MIXINS, "railroad.project.creation.use_mixins"), + "railroad.project.creation.use_mixins.info"), + described( + FormComponent.checkBox(ForgeProjectKeys.USE_ACCESS_TRANSFORMER, "railroad.project.creation.use_access_transformer"), + "railroad.project.creation.use_access_transformer.info"), + described( + FormComponent.checkBox(ForgeProjectKeys.GEN_RUN_FOLDERS, "railroad.project.creation.gen_run_folders"), + "railroad.project.creation.gen_run_folders.info")) + .onEnter(ctx -> { + String projectName = ctx.get(ProjectData.DefaultKeys.NAME); + + if (!isNullOrBlank(projectName)) { + modIdProperty.set(ProjectValidators.projectNameToModId(projectName)); + } + + if (!isNullOrBlank(projectName)) { + modNameProperty.set(projectName); + } + + if (!isNullOrBlank(projectName)) { + String mainClassName = ProjectValidators.projectNameToMainClass(projectName); + mainClassProperty.set(isNullOrBlank(mainClassName) ? "" : mainClassName); + } + }) + .build(); + } + + private OnboardingStep createLicenseStep() { + ObservableList availableLicenses = FXCollections.observableArrayList(); + ObjectProperty> licenseComboBox = new SimpleObjectProperty<>(); + BooleanProperty showCustomLicense = new SimpleBooleanProperty(false); + ChangeListener licenseSelectionListener = (observable, oldValue, newValue) -> + showCustomLicense.set(newValue == LicenseRegistry.CUSTOM); + + licenseComboBox.addListener((observable, oldValue, newValue) -> { + if (oldValue != null) { + oldValue.valueProperty().removeListener(licenseSelectionListener); + } + + if (newValue != null) { + showCustomLicense.set(newValue.getValue() == LicenseRegistry.CUSTOM); + newValue.valueProperty().addListener(licenseSelectionListener); + } else { + showCustomLicense.set(false); + } + }); + + BooleanBinding customLicenseVisible = Bindings.createBooleanBinding(showCustomLicense::get, showCustomLicense); + + return OnboardingFormStep.builder() + .id("license") + .title("railroad.project.creation.license.title") + .description("railroad.project.creation.license.description") + .appendSection("railroad.project.creation.section.license", + described( + FormComponent.comboBox(ProjectData.DefaultKeys.LICENSE, "railroad.project.creation.license", License.class) + .required() + .bindComboBoxTo(licenseComboBox) + .keyFunction(License::getSpdxId) + .valueOfFunction(License::fromSpdxId) + .defaultDisplayNameFunction(License::getName) + .translate(false) + .items(() -> availableLicenses) + .defaultValue(() -> { + if (availableLicenses.contains(LicenseRegistry.LGPL)) + return LicenseRegistry.LGPL; + + if (!availableLicenses.isEmpty()) + return availableLicenses.getFirst(); + + return null; + }), + "railroad.project.creation.license.info"), + described( + FormComponent.textField(ProjectData.DefaultKeys.LICENSE_CUSTOM, "railroad.project.creation.license.custom") + .visible(customLicenseVisible) + .promptText("railroad.project.creation.license.custom.prompt") + .validator(ProjectValidators::validateCustomLicense), + "railroad.project.creation.license.custom.info")) + .onEnter(ctx -> { + List newValues = License.REGISTRY.values() + .stream() + .sorted(Comparator.comparing(License::getName)) + .toList(); + + if (availableLicenses.size() != newValues.size() || !ListUtils.isEqualList(availableLicenses, newValues)) { + availableLicenses.clear(); + availableLicenses.addAll(newValues); + ctx.markForRefresh(ProjectData.DefaultKeys.LICENSE); + } + }) + .build(); + } + + private OnboardingStep createGitStep() { + return OnboardingFormStep.builder() + .id("git") + .title("railroad.project.creation.git.title") + .description("railroad.project.creation.git.description") + .appendSection("railroad.project.creation.section.git", + described( + FormComponent.checkBox(ProjectData.DefaultKeys.INIT_GIT, "railroad.project.creation.init_git") + .selected(true), + "railroad.project.creation.init_git.info")) + .build(); + } + + private OnboardingStep createOptionalDetailsStep() { + String configuredAuthor = SettingsHandler.getValue(Settings.DEFAULT_PROJECT_AUTHOR); + String defaultAuthor = !isNullOrBlank(configuredAuthor) + ? configuredAuthor + : Optional.ofNullable(System.getProperty("user.name")) + .filter(name -> !isNullOrBlank(name)) + .orElse(""); + + return OnboardingFormStep.builder() + .id("optional_details") + .title("railroad.project.creation.optional_details.title") + .description("railroad.project.creation.optional_details.description") + .appendSection("railroad.project.creation.section.optional_details", + described( + FormComponent.textField(ProjectData.DefaultKeys.AUTHOR, "railroad.project.creation.author") + .text(() -> defaultAuthor) + .promptText("railroad.project.creation.author.prompt") + .validator(ProjectValidators::validateAuthor), + "railroad.project.creation.author.info"), + described( + FormComponent.textField(ProjectData.DefaultKeys.CREDITS, "railroad.project.creation.credits") + .promptText("railroad.project.creation.credits.prompt") + .validator(ProjectValidators::validateCredits), + "railroad.project.creation.credits.info"), + described( + FormComponent.textArea(ProjectData.DefaultKeys.DESCRIPTION, "railroad.project.creation.description") + .promptText("railroad.project.creation.description.prompt") + .validator(ProjectValidators::validateDescription), + "railroad.project.creation.description.info"), + described( + FormComponent.textField(ProjectData.DefaultKeys.ISSUES_URL, "railroad.project.creation.issues_url") + .promptText("railroad.project.creation.issues_url.prompt") + .validator(ProjectValidators::validateIssues), + "railroad.project.creation.issues_url.info"), + described( + FormComponent.textField(ForgeProjectKeys.UPDATE_JSON_URL, "railroad.project.creation.update_json_url") + .promptText("railroad.project.creation.update_json_url.prompt") + .validator(ProjectValidators::validateUpdateJsonUrl), + "railroad.project.creation.update_json_url.info"), + described( + FormComponent.textField(ForgeProjectKeys.DISPLAY_URL, "railroad.project.creation.display_url") + .promptText("railroad.project.creation.display_url.prompt") + .validator(field -> ProjectValidators.validateGenericUrl(field, "display_url")), + "railroad.project.creation.display_url.info"), + described( + FormComponent.comboBox(ForgeProjectKeys.DISPLAY_TEST, "railroad.project.creation.display_test", DisplayTest.class) + .items(() -> Arrays.asList(DisplayTest.values())) + .defaultValue(() -> DisplayTest.MATCH_VERSION) + .keyFunction(DisplayTest::name) + .valueOfFunction(DisplayTest::valueOf) + .translate(false), + "railroad.project.creation.display_test.info"), + described( + FormComponent.checkBox(ForgeProjectKeys.CLIENT_SIDE_ONLY, "railroad.project.creation.client_side_only"), + "railroad.project.creation.client_side_only.info")) + .build(); + } + + private static OnboardingFormStep.ComponentSpec described(FormComponentBuilder builder, String descriptionKey) { + return OnboardingFormStep.component(builder, createDescriptionCustomizer(descriptionKey)); + } + + private static OnboardingFormStep.ComponentSpec described(FormComponentBuilder builder, Function transformer, Function reverseTransformer, String descriptionKey) { + return OnboardingFormStep.component(builder, builder != null ? builder.dataKey() : null, transformer, reverseTransformer, createDescriptionCustomizer(descriptionKey)); + } + + private static Consumer> createDescriptionCustomizer(String descriptionKey) { + if (isNullOrBlank(descriptionKey)) + return null; + + return component -> attachDescription(component, descriptionKey); + } + + private static void attachDescription(FormComponent component, String descriptionKey) { + if (component == null || isNullOrBlank(descriptionKey)) + return; + + Consumer applyToNode = node -> { + if (node instanceof InformativeLabeledHBox informative) { + boolean exists = informative.getInformationLabels().stream() + .anyMatch(label -> descriptionKey.equals(label.getKey())); + if (!exists) { + informative.addInformationLabel(descriptionKey); + } + } + }; + + Node currentNode = component.componentProperty().get(); + if (currentNode != null) { + applyToNode.accept(currentNode); + } + + component.componentProperty().addListener((observable, oldValue, newValue) -> { + if (newValue != null) { + applyToNode.accept(newValue); + } + }); + } + + private static MinecraftVersion getMinecraftVersion(String string) { + try { + return SwitchboardRepositories.MINECRAFT.getVersionSync(string).orElse(null); + } catch (ExecutionException | InterruptedException exception) { + Railroad.LOGGER.error("Failed to fetch Minecraft version {}", string, exception); + return null; + } + } + + private static CompletableFuture> resolveNeoforgeMinecraftVersions() { + return NEOFORGE_MINECRAFT_VERSIONS_CACHE.getAsync(() -> + SwitchboardRepositories.NEOFORGE.getAllVersions() + .thenApply(versions -> versions.stream() + .map(OldNeoforgeProjectOnboarding::extractMinecraftVersionId) + .flatMap(Optional::stream) + .map(OldNeoforgeProjectOnboarding::lookupMinecraftVersion) + .flatMap(Optional::stream) + .distinct() + .sorted(Comparator.reverseOrder()) + .toList()) + ); + } + + private static CompletableFuture fetchNeoforgeVersions(MinecraftVersion version) { + if (version == null) + return CompletableFuture.completedFuture(new NeoforgeVersionsPayload(null, List.of(), null)); + + String minecraftId = version.id(); + CompletableFuture> versionsFuture = SwitchboardRepositories.NEOFORGE.getVersionsFor(minecraftId); + CompletableFuture latestFuture = SwitchboardRepositories.NEOFORGE.getLatestVersionFor(minecraftId); + + return versionsFuture.thenCombine(latestFuture, (versions, latest) -> + new NeoforgeVersionsPayload(version, versions == null ? List.of() : versions, latest)) + .exceptionally(throwable -> { + Railroad.LOGGER.error("Failed to fetch Neoforge versions for Minecraft {}", version, throwable); + return new NeoforgeVersionsPayload(version, List.of(), null); + }); + } + + private static MinecraftVersion determineDefaultMinecraftVersion(List versions) { + if (versions == null || versions.isEmpty()) + return null; + + return versions.stream() + .filter(version -> version != null && version.getType() == MinecraftVersion.Type.RELEASE) + .findFirst() + .orElseGet(versions::getFirst); + } + + private static Optional lookupMinecraftVersion(String versionId) { + try { + return SwitchboardRepositories.MINECRAFT.getVersionSync(versionId); + } catch (ExecutionException exception) { + Railroad.LOGGER.error("Failed to fetch Minecraft version {}", versionId, exception); + } catch (InterruptedException exception) { + Thread.currentThread().interrupt(); + Railroad.LOGGER.error("Interrupted while fetching Minecraft version {}", versionId, exception); + } + + return Optional.empty(); + } + + private static Optional extractMinecraftVersionId(String neoforgeVersion) { + if (neoforgeVersion == null || neoforgeVersion.isBlank()) + return Optional.empty(); + + String lower = neoforgeVersion.toLowerCase(Locale.ROOT); + if (lower.contains("25w14craftmine")) + return Optional.of("25w14craftmine"); + + int lastDash = neoforgeVersion.indexOf('-'); + if (lastDash <= 0) + return Optional.empty(); + + String base = neoforgeVersion.substring(0, lastDash); + if (base.isBlank()) + return Optional.empty(); + + return Optional.of(base); + } + + private static boolean isRecommendedNeoforgeVersion(String version, String latest) { + return version != null && Objects.equals(version, latest) && !isNeoforgePrerelease(version); + } + + private static boolean isNeoforgePrerelease(String version) { + if (version == null) + return false; + + String lower = version.toLowerCase(Locale.ROOT); + return lower.contains("beta") || lower.contains("alpha") || lower.contains("rc") || lower.contains("25w14craftmine"); + } + + private static boolean isNullOrBlank(String value) { + return value == null || value.isBlank(); + } + + private static void bindTextField(StringProperty valueProperty, ObjectProperty fieldProperty) { + Objects.requireNonNull(valueProperty, "valueProperty"); + Objects.requireNonNull(fieldProperty, "fieldProperty"); + + valueProperty.addListener((obs, oldValue, newValue) -> { + TextField field = fieldProperty.get(); + if (field != null && !Objects.equals(field.getText(), newValue)) { + field.setText(newValue); + } + }); + + fieldProperty.addListener((obs, oldField, newField) -> { + if (newField == null) + return; + + if (!Objects.equals(newField.getText(), valueProperty.get())) { + newField.setText(valueProperty.get()); + } + + newField.textProperty().addListener((textObs, oldText, newText) -> { + if (!Objects.equals(valueProperty.get(), newText)) { + valueProperty.set(newText); + } + }); + }); + } + + private record NeoforgeVersionsPayload(MinecraftVersion contextVersion, List versions, String latest) { + } +} diff --git a/src/main/java/dev/railroadide/railroad/project/onboarding/impl/Onboarding.java b/src/main/java/dev/railroadide/railroad/project/onboarding/impl/Onboarding.java new file mode 100644 index 00000000..175d4e4e --- /dev/null +++ b/src/main/java/dev/railroadide/railroad/project/onboarding/impl/Onboarding.java @@ -0,0 +1,422 @@ +package dev.railroadide.railroad.project.onboarding.impl; + +import dev.railroadide.core.form.FormComponent; +import dev.railroadide.core.form.FormComponentBuilder; +import dev.railroadide.core.form.ui.InformativeLabeledHBox; +import dev.railroadide.core.project.License; +import dev.railroadide.core.project.ProjectData; +import dev.railroadide.core.switchboard.pojo.MinecraftVersion; +import dev.railroadide.railroad.Railroad; +import dev.railroadide.railroad.project.LicenseRegistry; +import dev.railroadide.railroad.project.ProjectValidators; +import dev.railroadide.railroad.project.data.MavenProjectKeys; +import dev.railroadide.railroad.project.data.MinecraftProjectKeys; +import dev.railroadide.railroad.project.onboarding.OnboardingContext; +import dev.railroadide.railroad.project.onboarding.step.OnboardingFormStep; +import dev.railroadide.railroad.project.onboarding.step.OnboardingStep; +import dev.railroadide.railroad.settings.Settings; +import dev.railroadide.railroad.settings.handler.SettingsHandler; +import dev.railroadide.railroad.switchboard.SwitchboardRepositories; +import javafx.beans.binding.Bindings; +import javafx.beans.binding.BooleanBinding; +import javafx.beans.property.*; +import javafx.beans.value.ChangeListener; +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; +import javafx.scene.Node; +import javafx.scene.Scene; +import javafx.scene.control.ComboBox; +import javafx.scene.control.TextField; +import org.apache.commons.collections.ListUtils; + +import java.nio.file.Path; +import java.util.Comparator; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicLong; +import java.util.function.Consumer; +import java.util.function.Function; + +public abstract class Onboarding { + public abstract void start(Scene scene); + + protected abstract void onFinish(OnboardingContext ctx, Scene scene); + + protected abstract List getMinecraftVersions(); + + protected OnboardingStep createProjectDetailsStep() { + return OnboardingFormStep.builder() + .id("project_details") + .title("railroad.project.creation.project_details.title") + .description("railroad.project.creation.project_details.description") + .appendSection("railroad.project.creation.section.project", + described( + FormComponent.textField(ProjectData.DefaultKeys.NAME, "railroad.project.creation.name") + .required() + .promptText("railroad.project.creation.name.prompt") + .validator(ProjectValidators::validateProjectName), + "railroad.project.creation.name.info"), + described( + FormComponent.directoryChooser(ProjectData.DefaultKeys.PATH, "railroad.project.creation.location") + .required() + .defaultPath(System.getProperty("user.home")) + .validator(ProjectValidators::validatePath), + value -> { + if (value == null) + return null; + + String text = value.toString(); + return text.isBlank() ? null : Path.of(text); + }, + value -> { + if (value == null) + return null; + + return value instanceof Path path ? path.toAbsolutePath().toString() : value.toString(); + }, + "railroad.project.creation.location.info")) + .build(); + } + + protected OnboardingStep createMavenCoordinatesStep() { + StringProperty artifactId = new SimpleStringProperty(); + String configuredGroupId = SettingsHandler.getValue(Settings.DEFAULT_PROJECT_GROUP_ID); + String configuredVersion = SettingsHandler.getValue(Settings.DEFAULT_PROJECT_VERSION); + String defaultGroupId = isNullOrBlank(configuredGroupId) ? "" : configuredGroupId; + String defaultVersion = isNullOrBlank(configuredVersion) ? "1.0.0" : configuredVersion; + + return OnboardingFormStep.builder() + .id("maven_coordinates") + .title("railroad.project.creation.maven_coordinates.title") + .description("railroad.project.creation.maven_coordinates.description") + .appendSection("railroad.project.creation.section.maven_coordinates", + described( + FormComponent.textField(MavenProjectKeys.GROUP_ID, "railroad.project.creation.group_id") + .required() + .promptText("railroad.project.creation.group_id.prompt") + .text(() -> defaultGroupId) + .validator(ProjectValidators::validateGroupId), + "railroad.project.creation.group_id.info"), + described( + FormComponent.textField(MavenProjectKeys.ARTIFACT_ID, "railroad.project.creation.artifact_id") + .required() + .promptText("railroad.project.creation.artifact_id.prompt") + .text(artifactId::get) + .validator(ProjectValidators::validateArtifactId), + "railroad.project.creation.artifact_id.info"), + described( + FormComponent.textField(MavenProjectKeys.VERSION, "railroad.project.creation.version") + .required() + .promptText("railroad.project.creation.version.prompt") + .text(() -> defaultVersion) + .validator(ProjectValidators::validateVersion), + "railroad.project.creation.version.info")) + .onEnter(ctx -> { + String projectName = ctx.get(ProjectData.DefaultKeys.NAME); + if (projectName != null) { + String defaultArtifactId = ProjectValidators.projectNameToArtifactId(projectName); + if (isNullOrBlank(artifactId.get())) { + artifactId.set(defaultArtifactId); + } + } + }) + .build(); + } + + protected OnboardingStep createMinecraftVersionStep() { + ObservableList availableVersions = FXCollections.observableArrayList(); + var nextInvalidationTime = new AtomicLong(0L); + + return OnboardingFormStep.builder() + .id("minecraft_version") + .title("railroad.project.creation.minecraft_version.title") + .description("railroad.project.creation.minecraft_version.description") + .appendSection("railroad.project.creation.section.minecraft_version", + described( + FormComponent.comboBox(MinecraftProjectKeys.MINECRAFT_VERSION, "railroad.project.creation.minecraft_version", MinecraftVersion.class) + .items(() -> availableVersions) + .defaultValue(() -> MinecraftVersion.determineDefaultMinecraftVersion(availableVersions)) + .keyFunction(MinecraftVersion::id) + .valueOfFunction(Onboarding::getMinecraftVersion) + .required() + .translate(false), + "railroad.project.creation.minecraft_version.info")) + .onEnter(ctx -> { + if (availableVersions.isEmpty() || System.currentTimeMillis() > nextInvalidationTime.get()) { + availableVersions.clear(); + availableVersions.addAll(getMinecraftVersions()); + nextInvalidationTime.set(System.currentTimeMillis() + TimeUnit.MINUTES.toMillis(5)); + ctx.markForRefresh(MinecraftProjectKeys.MINECRAFT_VERSION); + } + }) + .build(); + } + + protected OnboardingStep createModDetailsStep() { + StringProperty modIdProperty = new SimpleStringProperty(); + StringProperty modNameProperty = new SimpleStringProperty(); + StringProperty mainClassProperty = new SimpleStringProperty(); + + ObjectProperty modIdField = new SimpleObjectProperty<>(); + ObjectProperty modNameField = new SimpleObjectProperty<>(); + ObjectProperty mainClassField = new SimpleObjectProperty<>(); + + bindTextField(modIdProperty, modIdField); + bindTextField(modNameProperty, modNameField); + bindTextField(mainClassProperty, mainClassField); + + return OnboardingFormStep.builder() + .id("mod_details") + .title("railroad.project.creation.mod_details.title") + .description("railroad.project.creation.mod_details.description") + .appendSection("railroad.project.creation.section.mod_details", + described( + FormComponent.textField(MinecraftProjectKeys.MOD_ID, "railroad.project.creation.mod_id") + .required() + .promptText("railroad.project.creation.mod_id.prompt") + .text(modIdProperty::get) + .bindTextFieldTo(modIdField) + .validator(ProjectValidators::validateModId), + "railroad.project.creation.mod_id.info"), + described( + FormComponent.textField(MinecraftProjectKeys.MOD_NAME, "railroad.project.creation.mod_name") + .required() + .promptText("railroad.project.creation.mod_name.prompt") + .text(modNameProperty::get) + .bindTextFieldTo(modNameField) + .validator(ProjectValidators::validateModName), + "railroad.project.creation.mod_name.info"), + described( + FormComponent.textField(MinecraftProjectKeys.MAIN_CLASS, "railroad.project.creation.main_class") + .required() + .promptText("railroad.project.creation.main_class.prompt") + .text(mainClassProperty::get) + .bindTextFieldTo(mainClassField) + .validator(ProjectValidators::validateMainClass), + "railroad.project.creation.main_class.info")) + .onEnter(ctx -> { + String projectName = ctx.get(ProjectData.DefaultKeys.NAME); + + if (!isNullOrBlank(projectName)) { + modIdProperty.set(ProjectValidators.projectNameToModId(projectName)); + } + + if (!isNullOrBlank(projectName)) { + modNameProperty.set(projectName); + } + + if (!isNullOrBlank(projectName)) { + String mainClassName = ProjectValidators.projectNameToMainClass(projectName); + mainClassProperty.set(isNullOrBlank(mainClassName) ? "" : mainClassName); + } + }) + .build(); + } + + protected OnboardingStep createLicenseStep() { + ObservableList availableLicenses = FXCollections.observableArrayList(); + ObjectProperty> licenseComboBox = new SimpleObjectProperty<>(); + BooleanProperty showCustomLicense = new SimpleBooleanProperty(false); + ChangeListener licenseSelectionListener = (observable, oldValue, newValue) -> + showCustomLicense.set(newValue == LicenseRegistry.CUSTOM); + + licenseComboBox.addListener((observable, oldValue, newValue) -> { + if (oldValue != null) { + oldValue.valueProperty().removeListener(licenseSelectionListener); + } + + if (newValue != null) { + showCustomLicense.set(newValue.getValue() == LicenseRegistry.CUSTOM); + newValue.valueProperty().addListener(licenseSelectionListener); + } else { + showCustomLicense.set(false); + } + }); + + BooleanBinding customLicenseVisible = Bindings.createBooleanBinding(showCustomLicense::get, showCustomLicense); + + return OnboardingFormStep.builder() + .id("license") + .title("railroad.project.creation.license.title") + .description("railroad.project.creation.license.description") + .appendSection("railroad.project.creation.section.license", + described( + FormComponent.comboBox(ProjectData.DefaultKeys.LICENSE, "railroad.project.creation.license", License.class) + .required() + .bindComboBoxTo(licenseComboBox) + .keyFunction(License::getSpdxId) + .valueOfFunction(License::fromSpdxId) + .defaultDisplayNameFunction(License::getName) + .translate(false) + .items(() -> availableLicenses) + .defaultValue(() -> { + if (availableLicenses.contains(LicenseRegistry.LGPL)) + return LicenseRegistry.LGPL; + + if (!availableLicenses.isEmpty()) + return availableLicenses.getFirst(); + + return null; + }), + "railroad.project.creation.license.info"), + described( + FormComponent.textField(ProjectData.DefaultKeys.LICENSE_CUSTOM, "railroad.project.creation.license.custom") + .visible(customLicenseVisible) + .promptText("railroad.project.creation.license.custom.prompt") + .validator(ProjectValidators::validateCustomLicense), + "railroad.project.creation.license.custom.info")) + .onEnter(ctx -> { + List newValues = License.REGISTRY.values() + .stream() + .sorted(Comparator.comparing(License::getName)) + .toList(); + + if (availableLicenses.size() != newValues.size() || !ListUtils.isEqualList(availableLicenses, newValues)) { + availableLicenses.clear(); + availableLicenses.addAll(newValues); + ctx.markForRefresh(ProjectData.DefaultKeys.LICENSE); + } + }) + .build(); + } + + protected OnboardingStep createGitStep() { + // TODO: Provide options for GitHub, GitLab, Bitbucket initialization (with protected/public options) + return OnboardingFormStep.builder() + .id("git") + .title("railroad.project.creation.git.title") + .description("railroad.project.creation.git.description") + .appendSection("railroad.project.creation.section.git", + described( + FormComponent.checkBox(ProjectData.DefaultKeys.INIT_GIT, "railroad.project.creation.init_git") + .selected(true), + "railroad.project.creation.init_git.info")) + .build(); + } + + protected OnboardingStep createOptionalDetailsStep() { + String configuredAuthor = SettingsHandler.getValue(Settings.DEFAULT_PROJECT_AUTHOR); + String defaultAuthor = !isNullOrBlank(configuredAuthor) + ? configuredAuthor + : Optional.ofNullable(System.getProperty("user.name")) + .filter(name -> !isNullOrBlank(name)) + .orElse(""); + + return OnboardingFormStep.builder() + .id("optional_details") + .title("railroad.project.creation.optional_details.title") + .description("railroad.project.creation.optional_details.description") + .appendSection("railroad.project.creation.section.optional_details", + described( + FormComponent.textField(ProjectData.DefaultKeys.AUTHOR, "railroad.project.creation.author") + .text(() -> defaultAuthor) + .promptText("railroad.project.creation.author.prompt") + .validator(ProjectValidators::validateAuthor), + "railroad.project.creation.author.info"), + described( + FormComponent.textArea(ProjectData.DefaultKeys.DESCRIPTION, "railroad.project.creation.description") + .promptText("railroad.project.creation.description.prompt") + .validator(ProjectValidators::validateDescription), + "railroad.project.creation.description.info"), + described( + FormComponent.textField(ProjectData.DefaultKeys.ISSUES_URL, "railroad.project.creation.issues_url") + .promptText("railroad.project.creation.issues_url.prompt") + .validator(ProjectValidators::validateIssues), + "railroad.project.creation.issues_url.info"), + described( + FormComponent.textField(ProjectData.DefaultKeys.HOMEPAGE_URL, "railroad.project.creation.homepage_url") + .promptText("railroad.project.creation.homepage_url.prompt") + .validator(textField -> ProjectValidators.validateGenericUrl(textField, "homepage")), + "railroad.project.creation.homepage_url.info"), + described( + FormComponent.textField(ProjectData.DefaultKeys.SOURCES_URL, "railroad.project.creation.sources_url") + .promptText("railroad.project.creation.sources_url.prompt") + .validator(textField -> ProjectValidators.validateGenericUrl(textField, "sources")), + "railroad.project.creation.sources_url.info")) + .build(); + } + + protected static OnboardingFormStep.ComponentSpec described(FormComponentBuilder builder, String descriptionKey) { + return OnboardingFormStep.component(builder, createDescriptionCustomizer(descriptionKey)); + } + + protected static OnboardingFormStep.ComponentSpec described(FormComponentBuilder builder, Function transformer, Function reverseTransformer, String descriptionKey) { + return OnboardingFormStep.component(builder, builder != null ? builder.dataKey() : null, transformer, reverseTransformer, createDescriptionCustomizer(descriptionKey)); + } + + protected static Consumer> createDescriptionCustomizer(String descriptionKey) { + if (isNullOrBlank(descriptionKey)) + return null; + + return component -> attachDescription(component, descriptionKey); + } + + protected static void attachDescription(FormComponent component, String descriptionKey) { + if (component == null || isNullOrBlank(descriptionKey)) + return; + + Consumer applyToNode = node -> { + if (node instanceof InformativeLabeledHBox informative) { + boolean exists = informative.getInformationLabels().stream() + .anyMatch(label -> descriptionKey.equals(label.getKey())); + if (!exists) { + informative.addInformationLabel(descriptionKey); + } + } + }; + + Node currentNode = component.componentProperty().get(); + if (currentNode != null) { + applyToNode.accept(currentNode); + } + + component.componentProperty().addListener((observable, oldValue, newValue) -> { + if (newValue != null) { + applyToNode.accept(newValue); + } + }); + } + + protected static MinecraftVersion getMinecraftVersion(String string) { + try { + return SwitchboardRepositories.MINECRAFT.getVersionSync(string).orElse(null); + } catch (ExecutionException | InterruptedException exception) { + Railroad.LOGGER.error("Failed to fetch Minecraft version {}", string, exception); + return null; + } + } + + protected static boolean isNullOrBlank(String value) { + return value == null || value.isBlank(); + } + + protected static void bindTextField(StringProperty valueProperty, ObjectProperty fieldProperty) { + Objects.requireNonNull(valueProperty, "valueProperty"); + Objects.requireNonNull(fieldProperty, "fieldProperty"); + + valueProperty.addListener((obs, oldValue, newValue) -> { + TextField field = fieldProperty.get(); + if (field != null && !Objects.equals(field.getText(), newValue)) { + field.setText(newValue); + } + }); + + fieldProperty.addListener((obs, oldField, newField) -> { + if (newField == null) + return; + + if (!Objects.equals(newField.getText(), valueProperty.get())) { + newField.setText(valueProperty.get()); + } + + newField.textProperty().addListener((textObs, oldText, newText) -> { + if (!Objects.equals(valueProperty.get(), newText)) { + valueProperty.set(newText); + } + }); + }); + } +}