diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml new file mode 100644 index 0000000..3d892a2 --- /dev/null +++ b/.github/workflows/pr.yml @@ -0,0 +1,27 @@ +name: PR Build + +on: + pull_request: + branches: + - master + +jobs: + test: + name: Run Tests + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up JDK 25 + uses: actions/setup-java@v4 + with: + java-version: 25 + distribution: temurin + + - name: Grant Gradle permissions + run: chmod +x ./gradlew + + - name: Run tests + run: ./gradlew test diff --git a/build.gradle b/build.gradle index d546a48..c2d20cc 100644 --- a/build.gradle +++ b/build.gradle @@ -45,6 +45,7 @@ dependencies { implementation 'com.fasterxml.jackson.module:jackson-module-kotlin' implementation 'com.fasterxml.jackson.dataformat:jackson-dataformat-yaml' implementation 'com.github.Frotty:SimpleRegistry:f96dda96bd' + implementation 'org.jline:jline:3.30.13' implementation group: 'org.slf4j', name: 'slf4j-api', version: '2.0.17' implementation group: 'ch.qos.logback', name: 'logback-classic', version: '1.5.32' testImplementation 'org.testng:testng:7.12.0' diff --git a/src/main/kotlin/config/DAOs.kt b/src/main/kotlin/config/DAOs.kt index bd19436..912b7b5 100644 --- a/src/main/kotlin/config/DAOs.kt +++ b/src/main/kotlin/config/DAOs.kt @@ -7,7 +7,6 @@ import kotlin.collections.ArrayList const val CONFIG_FILE_NAME = "wurst.build" enum class ScriptMode { LUA, JASS } -enum class Wc3Patch { REFORGED, PRE_129 } /** * The root DAO that contains the child DAOs. @@ -19,7 +18,7 @@ data class WurstProjectConfigData( val dependencies: ArrayList = ArrayList(), val buildMapData: WurstProjectBuildMapData = WurstProjectBuildMapData(), val scriptMode: ScriptMode? = null, - val wc3Patch: Wc3Patch? = null + var wc3Patch: String? = null ) { constructor() : this("unnamed") } diff --git a/src/main/kotlin/file/CLICommand.kt b/src/main/kotlin/file/CLICommand.kt index 2e4f6fb..23af697 100644 --- a/src/main/kotlin/file/CLICommand.kt +++ b/src/main/kotlin/file/CLICommand.kt @@ -1,7 +1,6 @@ package file import config.ScriptMode -import config.Wc3Patch enum class CLICommand { HELP, @@ -76,10 +75,7 @@ enum class GlobalOptions(val optionName: String = "", val argCount: Int = 0) { }, WC3_PATCH("--wc3-patch", 1) { override fun runOption(setupMain: SetupMain, args: List) { - setupMain.wc3Patch = when (args[0].lowercase()) { - "pre1.29" -> Wc3Patch.PRE_129 - else -> Wc3Patch.REFORGED - } + setupMain.wc3Patch = CoreJassProvider.normalizePatchInput(args[0]) } }; diff --git a/src/main/kotlin/file/CoreJassProvider.kt b/src/main/kotlin/file/CoreJassProvider.kt new file mode 100644 index 0000000..a88f545 --- /dev/null +++ b/src/main/kotlin/file/CoreJassProvider.kt @@ -0,0 +1,264 @@ +package file + +import logging.KotlinLogging +import java.net.URI +import java.nio.file.AtomicMoveNotSupportedException +import java.nio.file.Files +import java.nio.file.Path +import java.nio.file.StandardCopyOption +import java.util.jar.JarFile + +object CoreJassProvider { + const val DEFAULT_PATCH = "v1.36" + const val PRE_129_PATCH = "v1.28" + + private const val JASS_HISTORY_RAW = "https://raw.githubusercontent.com/Luashine/jass-history" + private const val JASS_HISTORY_REF = "master" + private val log = KotlinLogging.logger {} + + private val PATCH_TO_JASS_HISTORY_FOLDER = linkedMapOf( + "v1.36" to "Reforged-v1.36.1.20719-w3-51d40ee", + "v1.35" to "Reforged-v1.35.0.20093-w3-5ec1b77", + "v1.34" to "Reforged-v1.34.0.19632-w3-31590bf", + "v1.33" to "Reforged-v1.33.0.19378-w3-e94d62c", + "v1.32" to "Reforged-v1.32.10.19202", + "v1.31" to "TFT-v1.31.1.12173", + "v1.30" to "TFT-v1.30.4.11274", + "v1.29" to "TFT-v1.29.2.9231", + "v1.28" to "TFT-v1.28.2.7395", + "v1.27b" to "TFT-v1.27b-ru", + "v1.27a" to "TFT-v1.27a-ru", + "v1.26a" to "TFT-v1.26a-ru", + "v1.25b" to "TFT-v1.25b-ru", + "v1.24e" to "TFT-v1.24e-ru", + "v1.24d" to "TFT-v1.24d-ru", + "v1.24c" to "TFT-v1.24c-ru", + "v1.24b" to "TFT-v1.24b-ru", + "v1.24a" to "TFT-v1.24a-ru", + "v1.23a" to "TFT-v1.23a-ru", + "v1.22a" to "TFT-v1.22a-ru", + "v1.21b" to "TFT-v1.21b-ru", + "v1.21a" to "TFT-v1.21a-ru", + "v1.21" to "Beta-ROC-v1.21", + "v1.20e" to "TFT-v1.20e-ru", + "v1.20d" to "TFT-v1.20d-ru", + "v1.20c" to "TFT-v1.20c-ru", + "v1.20b" to "TFT-v1.20b-ru", + "v1.20a" to "TFT-v1.20a-ru", + "v1.20" to "Beta-ROC-v1.20", + "v1.19b" to "TFT-v1.19b-ru", + "v1.19a" to "TFT-v1.19a-ru", + "v1.18a" to "TFT-v1.18a-ru", + "v1.17a" to "TFT-v1.17a-ru", + "v1.16a" to "TFT-v1.16a-ru", + "v1.15" to "TFT-v1.15-ru", + "v1.14b" to "TFT-v1.14b-ru", + "v1.14" to "TFT-v1.14-ru", + "v1.13b" to "TFT-v1.13b-ru", + "v1.13" to "TFT-v1.13-ru", + "v1.12" to "TFT-v1.12-ru", + "v1.11" to "TFT-v1.11-ru", + "v1.10" to "TFT-v1.10-ru", + "v1.07" to "TFT-v1.07-ru", + "v1.06" to "ROC-v1.06-ru", + "v1.05" to "ROC-v1.05-ru", + "v1.04" to "ROC-v1.04-ru", + "v1.03" to "ROC-v1.03-ru", + "v1.02a" to "ROC-v1.02a-ru", + "v1.02" to "ROC-v1.02-ru", + "v1.01b" to "ROC-v1.01b-ru", + "v1.01" to "ROC-v1.01-ru", + "v1.00" to "ROC-v1.00-ru" + ) + + val supportedPatches: List = PATCH_TO_JASS_HISTORY_FOLDER.keys.toList() + + fun describePatch(patch: String): String { + val normalizedPatch = normalizePatchInput(patch) + val label = when (normalizedPatch) { + DEFAULT_PATCH -> "latest Reforged / WC3 2.x core JASS" + "v1.31" -> "latest classic TFT" + PRE_129_PATCH -> "legacy pre-1.29" + else -> { + val minor = Regex("""v1\.(\d+)""").find(normalizedPatch)?.groupValues?.get(1)?.toIntOrNull() + when { + minor != null && minor >= 32 -> "Reforged" + minor != null && minor >= 7 -> "classic TFT" + minor != null -> "classic ROC" + else -> null + } + } + } + return if (label == null) normalizedPatch else "$normalizedPatch ($label)" + } + + fun jassHistoryFolderForPatch(patch: String): String? { + return PATCH_TO_JASS_HISTORY_FOLDER[normalizePatchInput(patch)] + } + + fun isSupportedPatch(patch: String): Boolean { + return PATCH_TO_JASS_HISTORY_FOLDER.containsKey(normalizePatchInput(patch)) + } + + fun normalizePatchInput(input: String?): String { + val patch = input?.trim().orEmpty() + val normalizedAlias = when (patch.lowercase()) { + "", "reforged", "latest" -> DEFAULT_PATCH + "pre1.29", "pre-1.29", "pre_129", "pre-129" -> PRE_129_PATCH + else -> patch + } + val withPrefix = if (normalizedAlias.matches(Regex("""\d+\.\d+.*"""))) "v$normalizedAlias" else normalizedAlias + val canonicalCase = PATCH_TO_JASS_HISTORY_FOLDER.keys.firstOrNull { it.equals(withPrefix, ignoreCase = true) } + if (canonicalCase != null) { + return canonicalCase + } + return PATCH_TO_JASS_HISTORY_FOLDER.entries.firstOrNull { it.value.equals(withPrefix, ignoreCase = true) }?.key + ?: withPrefix + } + + fun isPre129Patch(input: String?): Boolean { + val patch = normalizePatchInput(input) + if (patch == PRE_129_PATCH) { + return true + } + val version = Regex("""v(\d+)\.(\d+)""").find(patch)?.groupValues ?: return false + return version[1].toIntOrNull() == 1 && (version[2].toIntOrNull() ?: 29) < 29 + } + + fun ensureFiles(projectRoot: Path, wc3Patch: String?): List { + val buildFolder = projectRoot.resolve("_build") + Files.createDirectories(buildFolder) + val patch = normalizePatchInput(wc3Patch) + val previousPatch = readProvenance(buildFolder) + val materializedFiles = listOf( + materializeFile(buildFolder, "common.j", patch, previousPatch), + materializeFile(buildFolder, "blizzard.j", patch, previousPatch) + ) + if (materializedFiles.all { it.managedByGrill }) { + Files.writeString( + buildFolder.resolve("core-jass.provenance"), + "wc3Patch: $patch\njassHistoryFolder: ${jassHistoryFolderForPatch(patch).orEmpty()}\n" + ) + } else { + log.warn( + "Existing _build core JASS files have no Grill provenance; leaving them project-owned. " + + "Delete _build/common.j and _build/blizzard.j to let Grill regenerate them for $patch." + ) + } + return materializedFiles.map { it.path } + } + + fun fetchJassHistoryVersions(): List { + return supportedPatches + } + + fun recommendedPatchOptions(versions: List): List { + return (listOf(DEFAULT_PATCH, "v1.31", PRE_129_PATCH) + versions.take(1)).distinct() + } + + private data class MaterializedFile(val path: Path, val managedByGrill: Boolean) + + private fun materializeFile(buildFolder: Path, fileName: String, patch: String, previousPatch: String?): MaterializedFile { + val target = buildFolder.resolve(fileName) + val jassHistoryFolder = PATCH_TO_JASS_HISTORY_FOLDER[patch] + if (jassHistoryFolder == null) { + throw IllegalArgumentException("Unsupported WC3 patch <$patch>. Supported values: ${supportedPatches.joinToString()}") + } + + if (previousPatch == null && Files.exists(target)) { + log.info("Keeping existing _build/$fileName because it has no Grill provenance.") + return MaterializedFile(target, managedByGrill = false) + } + + val canKeepExisting = previousPatch == patch + if (canKeepExisting && isValidCoreJassFile(target)) { + log.info("Using cached _build/$fileName for $patch.") + return MaterializedFile(target, managedByGrill = true) + } + + try { + downloadJassHistoryFile(fileName, patch, jassHistoryFolder, target) + return MaterializedFile(target, managedByGrill = true) + } catch (e: Exception) { + if (canKeepExisting && isValidCoreJassFile(target)) { + log.warn("Could not refresh $fileName for $patch; keeping existing _build copy. Reason: ${e.message}") + return MaterializedFile(target, managedByGrill = true) + } + if (patch == DEFAULT_PATCH || patch == PRE_129_PATCH) { + log.warn("Could not download $fileName for $patch; falling back to bundled core JASS. Reason: ${e.message}") + copyBundledCoreJass(fileName, patch, target) + return MaterializedFile(target, managedByGrill = true) + } + throw RuntimeException("Could not download $fileName for WC3 patch <$patch> from jass-history.", e) + } + } + + private fun readProvenance(buildFolder: Path): String? { + val provenanceFile = buildFolder.resolve("core-jass.provenance") + if (!Files.exists(provenanceFile)) { + return null + } + return Files.readString(provenanceFile) + .lineSequence() + .firstOrNull { it.startsWith("wc3Patch:") } + ?.substringAfter(":") + ?.trim() + ?.let(::normalizePatchInput) + } + + private fun copyBundledCoreJass(fileName: String, patch: String, target: Path) { + val patchFolder = when (patch) { + PRE_129_PATCH -> "pre1.29" + else -> "reforged" + } + + Files.createDirectories(target.parent) + val resourcePath = "core-jass/$patchFolder/$fileName" + javaClass.classLoader.getResourceAsStream(resourcePath)?.use { input -> + Files.copy(input, target, StandardCopyOption.REPLACE_EXISTING) + return + } + + JarFile(global.InstallationManager.getCompilerPath()).use { jar -> + val entry = jar.getEntry(fileName) + ?: throw IllegalStateException("Bundled $fileName was not found for $patch.") + jar.getInputStream(entry).use { input -> + Files.copy(input, target, StandardCopyOption.REPLACE_EXISTING) + } + } + } + + private fun downloadJassHistoryFile(fileName: String, patch: String, jassHistoryFolder: String, target: Path) { + Files.createDirectories(target.parent) + val rawUrl = "$JASS_HISTORY_RAW/$JASS_HISTORY_REF/war3extract/$jassHistoryFolder/scripts/$fileName" + val tempFile = Files.createTempFile(target.parent, "$fileName.", ".download") + var replacedTarget = false + try { + URI(rawUrl).toURL().openStream().use { input -> + Files.copy(input, tempFile, StandardCopyOption.REPLACE_EXISTING) + } + if (!isValidCoreJassFile(tempFile)) { + throw IllegalStateException("Downloaded $fileName from jass-history did not look valid.") + } + moveValidatedDownload(tempFile, target) + replacedTarget = true + } finally { + if (!replacedTarget) { + Files.deleteIfExists(tempFile) + } + } + } + + private fun isValidCoreJassFile(path: Path): Boolean { + return Files.exists(path) && Files.size(path) >= 1024L + } + + private fun moveValidatedDownload(source: Path, target: Path) { + try { + Files.move(source, target, StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.ATOMIC_MOVE) + } catch (_: AtomicMoveNotSupportedException) { + Files.move(source, target, StandardCopyOption.REPLACE_EXISTING) + } + } + +} diff --git a/src/main/kotlin/file/SetupApp.kt b/src/main/kotlin/file/SetupApp.kt index 484fba5..995ac08 100644 --- a/src/main/kotlin/file/SetupApp.kt +++ b/src/main/kotlin/file/SetupApp.kt @@ -2,7 +2,6 @@ package file import config.CONFIG_FILE_NAME import config.ScriptMode -import config.Wc3Patch import config.WurstProjectConfig import config.WurstProjectConfigData import global.InstallationManager @@ -17,9 +16,7 @@ import java.net.URL import java.nio.file.Files import java.nio.file.Path import java.nio.file.Paths -import java.nio.file.StandardCopyOption import java.util.* -import java.util.jar.JarFile import javax.swing.JOptionPane @@ -140,7 +137,7 @@ object SetupApp { | |Generate options: | --script-mode lua|jass Script mode (default: lua) - | --wc3-patch reforged|pre1.29 WC3 patch target (default: reforged) + | --wc3-patch WC3 patch target: reforged, pre1.29, or jass-history version | --with-agents / --no-agents Include AGENTS.md (default: no) | --with-ci / --no-ci Include GitHub Actions workflow (default: no) """.trimMargin()) @@ -148,6 +145,7 @@ object SetupApp { setup.command == CLICommand.INSTALL -> { if (setup.commandArg.isBlank()) { if (configData != null) { + ensureProjectPatchRecorded(configData) handleUpdateProject(configData) } else { missingProject() @@ -156,9 +154,10 @@ object SetupApp { handleInstallWurst() } else if (setup.commandArg.lowercase() == "grill") { handleUpdateGrill() - } else { + } else { if (configData != null) { handleInstallDep(configData) + ensureProjectPatchRecorded(configData) WurstProjectConfig.saveProjectConfig(setup.projectRoot, configData) handleUpdateProject(configData) } else { @@ -184,7 +183,7 @@ object SetupApp { } log.info("✈ Generating project...") val projectDir = DEFAULT_DIR.resolve(setup.commandArg) - val stdlibUrl = if (setup.wc3Patch == Wc3Patch.PRE_129) + val stdlibUrl = if (CoreJassProvider.isPre129Patch(setup.wc3Patch)) "https://github.com/wurstscript/wurstStdlib2:pre1.29" else "https://github.com/wurstscript/wurstStdlib2" @@ -195,6 +194,7 @@ object SetupApp { wc3Patch = setup.wc3Patch ) WurstProjectConfig.handleCreate(projectDir, null, projectConfig) + ensureCoreJassFiles(projectDir, projectConfig.wc3Patch) if (Files.exists(projectDir)) { if (setup.addAgents) downloadAgentsMd(projectDir) if (setup.addGithubWorkflow) writeCiWorkflow(projectDir) @@ -354,6 +354,7 @@ object SetupApp { } internal var generatePrompt: ((String, String?) -> String?)? = null + internal var installPatchPrompt: ((String, String?) -> String?)? = null internal fun prepareGenerate(setup: SetupMain): Boolean { if (setup.commandArg.isNotBlank()) { @@ -371,7 +372,7 @@ object SetupApp { } } - runWizard(setup, prompt) + runWizard(setup, prompt, useInteractiveMenus = generatePrompt == null) return true } @@ -401,14 +402,14 @@ object SetupApp { } } - private fun runWizard(setup: SetupMain, prompt: (String, String?) -> String?) { - val scriptModeDefault = setup.scriptMode.name.lowercase() - val scriptModeInput = prompt("Script mode (lua/jass)", scriptModeDefault) - setup.scriptMode = if (scriptModeInput?.lowercase() == "jass") ScriptMode.JASS else ScriptMode.LUA - - val patchDefault = if (setup.wc3Patch == Wc3Patch.PRE_129) "pre1.29" else "reforged" - val patchInput = prompt("WC3 patch target (reforged/pre1.29)", patchDefault) - setup.wc3Patch = if (patchInput?.lowercase() == "pre1.29") Wc3Patch.PRE_129 else Wc3Patch.REFORGED + private fun runWizard(setup: SetupMain, prompt: (String, String?) -> String?, useInteractiveMenus: Boolean) { + setup.scriptMode = selectScriptMode(prompt, setup.scriptMode, useInteractiveMenus) + setup.wc3Patch = selectPatchVersion( + prompt, + intro = "WC3 patch choices:", + useInteractiveMenus = useInteractiveMenus, + currentPatch = setup.wc3Patch + ) val agentsDefault = if (setup.addAgents) "Y" else "N" val agentsInput = prompt("Add AGENTS.md?", agentsDefault) @@ -419,6 +420,188 @@ object SetupApp { setup.addGithubWorkflow = ciInput?.lowercase() == "y" } + private fun selectScriptMode( + prompt: (String, String?) -> String?, + defaultMode: ScriptMode, + useInteractiveMenus: Boolean + ): ScriptMode { + if (useInteractiveMenus) { + TerminalMenu.choose( + title = "Script mode", + choices = listOf( + TerminalMenu.Choice(ScriptMode.LUA, "lua"), + TerminalMenu.Choice(ScriptMode.JASS, "jass") + ), + defaultIndex = if (defaultMode == ScriptMode.JASS) 1 else 0 + )?.let { return it } + } + + log.info("Script mode choices:") + log.info(" 1. lua") + log.info(" 2. jass") + + while (true) { + val defaultValue = defaultMode.name.lowercase() + val answer = prompt("Script mode (number/name)", defaultValue)?.trim() + if (answer.isNullOrBlank()) { + return defaultMode + } + when (answer.lowercase()) { + "1", "lua" -> return ScriptMode.LUA + "2", "jass" -> return ScriptMode.JASS + else -> log.error("Unsupported script mode: $answer. Choose 1/lua or 2/jass.") + } + } + } + + private fun ensureProjectPatchRecorded(configData: WurstProjectConfigData) { + val currentPatch = configData.wc3Patch + if (currentPatch.isNullOrBlank()) { + val selectedPatch = selectPatchVersionForInstall() + configData.wc3Patch = selectedPatch + log.info("WC3 patch recorded in wurst.build: $selectedPatch") + return + } + + val normalizedPatch = CoreJassProvider.normalizePatchInput(currentPatch) + if (normalizedPatch != currentPatch) { + configData.wc3Patch = normalizedPatch + } + } + + internal fun selectPatchVersionForInstall(): String { + return selectPatchVersion( + installPatchPrompt ?: terminalPrompt(), + intro = "No WC3 patch is recorded in wurst.build yet.", + useInteractiveMenus = installPatchPrompt == null, + currentPatch = null + ) + } + + private fun selectPatchVersion( + prompt: (String, String?) -> String?, + intro: String, + useInteractiveMenus: Boolean, + currentPatch: String? + ): String { + val versions = CoreJassProvider.fetchJassHistoryVersions() + val recommended = CoreJassProvider.recommendedPatchOptions(versions) + val normalizedCurrentPatch = currentPatch?.let(CoreJassProvider::normalizePatchInput) + val defaultPatch = when { + normalizedCurrentPatch != null && CoreJassProvider.isSupportedPatch(normalizedCurrentPatch) -> normalizedCurrentPatch + else -> recommended.firstOrNull() ?: CoreJassProvider.DEFAULT_PATCH + } + val browseAll = "__browse_all__" + + if (useInteractiveMenus) { + while (true) { + val choices = recommended.map { TerminalMenu.Choice(it, CoreJassProvider.describePatch(it)) } + + TerminalMenu.Choice(browseAll, "Browse all supported versions...") + val selection = TerminalMenu.choose( + title = intro, + choices = choices, + defaultIndex = recommended.indexOf(defaultPatch).takeIf { it >= 0 } ?: 0 + ) + when { + selection == null -> return defaultPatch + selection == browseAll -> browsePatchVersionsInteractive(versions)?.let { return it } + else -> return selection + } + } + } + + log.info(intro) + val visibleRecommended = (listOf(defaultPatch) + recommended).distinct() + log.info("Recommended patch choices:") + visibleRecommended.forEachIndexed { index, patch -> + log.info(" ${index + 1}. ${CoreJassProvider.describePatch(patch)}") + } + if (versions.isNotEmpty()) { + log.info("Type `more` to browse all jass-history versions.") + } + log.info("Enter a listed number, press Enter for the default, or type `more`.") + + while (true) { + val answer = prompt("WC3 patch version (number/version/more)", defaultPatch)?.trim() + if (answer.isNullOrBlank()) { + return defaultPatch + } + val topIndex = answer.toIntOrNull() + if (topIndex != null && topIndex in 1..visibleRecommended.size) { + return visibleRecommended[topIndex - 1] + } + when (answer.lowercase()) { + "more", "list", "all" -> browsePatchVersions(versions, prompt)?.let { return it } + "q", "quit", "cancel" -> return defaultPatch + else -> { + val normalized = CoreJassProvider.normalizePatchInput(answer) + val directSelection = visibleRecommended.firstOrNull { it.equals(normalized, ignoreCase = true) } + if (directSelection != null) { + return directSelection + } + log.error("Unsupported patch selection: $answer") + log.info("Choose one of the listed numbers, or type `more` to browse all supported versions.") + } + } + } + } + + private fun browsePatchVersions(versions: List, prompt: (String, String?) -> String?): String? { + if (versions.isEmpty()) { + log.info("Could not load jass-history versions right now. You can still type a version folder manually.") + return null + } + + val pageSize = 20 + var page = 0 + while (true) { + val start = page * pageSize + val visibleVersions = versions.drop(start).take(pageSize) + if (visibleVersions.isEmpty()) { + page = 0 + continue + } + + log.info("WC3 patch versions ${start + 1}-${start + visibleVersions.size} of ${versions.size}:") + visibleVersions.forEachIndexed { index, version -> + log.info(" ${index + 1}. ${CoreJassProvider.describePatch(version)}") + } + val answer = prompt("Select version (number/version, n next, p previous, q back)", null)?.trim() + if (answer.isNullOrBlank()) { + return null + } + val pageIndex = answer.toIntOrNull() + if (pageIndex != null && pageIndex in 1..visibleVersions.size) { + return visibleVersions[pageIndex - 1] + } + when (answer.lowercase()) { + "n", "next" -> page = if (start + pageSize >= versions.size) 0 else page + 1 + "p", "prev", "previous" -> page = if (page == 0) (versions.size - 1) / pageSize else page - 1 + "q", "back", "cancel" -> return null + else -> { + val normalized = CoreJassProvider.normalizePatchInput(answer) + val directSelection = versions.firstOrNull { it.equals(normalized, ignoreCase = true) } + if (directSelection != null) { + return directSelection + } + log.error("Unsupported patch selection: $answer") + log.info("Choose a number from the current page, use `n`/`p`, or type `q` to go back.") + } + } + } + } + + private fun browsePatchVersionsInteractive(versions: List): String? { + if (versions.isEmpty()) { + return null + } + return TerminalMenu.choose( + title = "WC3 patch versions", + choices = versions.map { TerminalMenu.Choice(it, CoreJassProvider.describePatch(it)) }, + defaultIndex = 0 + ) + } + private fun downloadAgentsMd(projectDir: Path) { try { val content = URL("https://raw.githubusercontent.com/wurstscript/WurstSetup/master/templates/AGENTS.md").readText() @@ -567,14 +750,9 @@ object SetupApp { } } } else { - val common = resolveCoreJassFile(buildFolder, "common.j", configData.wc3Patch) - val blizzard = resolveCoreJassFile(buildFolder, "blizzard.j", configData.wc3Patch) - if (Files.exists(common)) { - args.add(common.toAbsolutePath().toString()) - } - if (Files.exists(blizzard)) { - args.add(blizzard.toAbsolutePath().toString()) - } + ensureCoreJassFiles(setup.projectRoot, configData.wc3Patch) + .filter { Files.exists(it) } + .forEach { args.add(it.toAbsolutePath().toString()) } } args.add(setup.projectRoot.resolve("wurst").toAbsolutePath().toString()) @@ -599,43 +777,8 @@ object SetupApp { return if (configData.scriptMode == ScriptMode.LUA) "output.lua" else "output.j" } - private fun resolveCoreJassFile(buildFolder: Path, fileName: String, wc3Patch: Wc3Patch?): Path { - val projectCopy = buildFolder.resolve(fileName) - if (Files.exists(projectCopy)) { - return projectCopy - } - - return extractCoreJassFile(buildFolder, fileName, wc3Patch ?: Wc3Patch.REFORGED) - } - - private fun extractCoreJassFile(buildFolder: Path, fileName: String, wc3Patch: Wc3Patch): Path { - val patchFolder = when (wc3Patch) { - Wc3Patch.PRE_129 -> "pre1.29" - Wc3Patch.REFORGED -> "reforged" - } - val target = buildFolder.resolve("grill").resolve("core-jass").resolve(patchFolder).resolve(fileName) - if (Files.exists(target)) { - return target - } - - try { - Files.createDirectories(target.parent) - val resourcePath = "core-jass/$patchFolder/$fileName" - javaClass.classLoader.getResourceAsStream(resourcePath)?.use { input -> - Files.copy(input, target, StandardCopyOption.REPLACE_EXISTING) - return target - } - - JarFile(InstallationManager.getCompilerPath()).use { jar -> - val entry = jar.getEntry(fileName) ?: return target - jar.getInputStream(entry).use { input -> - Files.copy(input, target, StandardCopyOption.REPLACE_EXISTING) - } - } - } catch (e: Exception) { - log.warn("Could not extract $fileName for patch $wc3Patch: ${e.message}") - } - return target + internal fun ensureCoreJassFiles(projectRoot: Path, wc3Patch: String?): List { + return CoreJassProvider.ensureFiles(projectRoot, wc3Patch) } private fun handleRemoveDep(configData: WurstProjectConfigData) { @@ -656,6 +799,7 @@ object SetupApp { private fun handleUpdateProject(configData: WurstProjectConfigData) { WurstProjectConfig.handleUpdate(setup.projectRoot, null, configData) + ensureCoreJassFiles(setup.projectRoot, configData.wc3Patch) } val REPO_REGEX = Regex("(https?://)([\\w.@-]+)(/)([\\w,-_]+)/([\\w,-_]+)(.git)?((/)?)") diff --git a/src/main/kotlin/file/SetupMain.kt b/src/main/kotlin/file/SetupMain.kt index 95b1265..92c017d 100644 --- a/src/main/kotlin/file/SetupMain.kt +++ b/src/main/kotlin/file/SetupMain.kt @@ -1,7 +1,6 @@ package file import config.ScriptMode -import config.Wc3Patch import logging.KotlinLogging import java.nio.file.Files import java.nio.file.Path @@ -30,7 +29,7 @@ class SetupMain { var addAgents: Boolean = false var addGithubWorkflow: Boolean = false var scriptMode: ScriptMode = ScriptMode.LUA - var wc3Patch: Wc3Patch = Wc3Patch.REFORGED + var wc3Patch: String = CoreJassProvider.DEFAULT_PATCH fun setProjectDir(dir: Path) { Files.createDirectories(dir) diff --git a/src/main/kotlin/file/TerminalMenu.kt b/src/main/kotlin/file/TerminalMenu.kt new file mode 100644 index 0000000..c4c2c91 --- /dev/null +++ b/src/main/kotlin/file/TerminalMenu.kt @@ -0,0 +1,122 @@ +package file + +import org.jline.keymap.BindingReader +import org.jline.keymap.KeyMap +import org.jline.terminal.Terminal +import org.jline.terminal.TerminalBuilder +import org.jline.utils.InfoCmp +import kotlin.math.max +import kotlin.math.min + +object TerminalMenu { + data class Choice(val value: T, val label: String) + + private const val UP = "up" + private const val DOWN = "down" + private const val LEFT = "left" + private const val RIGHT = "right" + private const val ENTER = "enter" + private const val CANCEL = "cancel" + + fun canUseInteractive(): Boolean { + return System.console() != null + } + + fun choose(title: String, choices: List>, defaultIndex: Int = 0): T? { + if (!canUseInteractive() || choices.isEmpty()) { + return null + } + + try { + TerminalBuilder.builder().system(true).dumb(false).build().use { terminal -> + val oldAttributes = terminal.enterRawMode() + val reader = BindingReader(terminal.reader()) + val keyMap = menuKeyMap(terminal) + val writer = terminal.writer() + var selected = defaultIndex.coerceIn(0, choices.lastIndex) + var firstVisible = 0 + var result: T? = null + var done = false + + fun render() { + val visibleRows = max(3, min(12, terminal.height - 5)) + if (selected < firstVisible) { + firstVisible = selected + } else if (selected >= firstVisible + visibleRows) { + firstVisible = selected - visibleRows + 1 + } + + clearScreen(terminal) + writer.print("\u001B[?25l") + writer.println(title) + writer.println("Use Up/Down, Enter to select, Esc to cancel.") + val end = min(choices.size, firstVisible + visibleRows) + for (index in firstVisible until end) { + val marker = if (index == selected) "> " else " " + writer.println(marker + choices[index].label) + } + if (firstVisible > 0 || end < choices.size) { + writer.println(" ${firstVisible + 1}-$end of ${choices.size}") + } + writer.flush() + } + + try { + while (!done) { + render() + when (reader.readBinding(keyMap)) { + ENTER -> { + result = choices[selected].value + done = true + } + UP -> selected = if (selected == 0) choices.lastIndex else selected - 1 + DOWN -> selected = if (selected == choices.lastIndex) 0 else selected + 1 + LEFT -> selected = max(0, selected - 10) + RIGHT -> selected = min(choices.lastIndex, selected + 10) + CANCEL -> done = true + else -> Unit + } + } + } finally { + terminal.attributes = oldAttributes + clearScreen(terminal) + writer.print("\u001B[?25h") + writer.flush() + } + return result + } + } catch (_: Exception) { + return null + } + } + + private fun menuKeyMap(terminal: Terminal): KeyMap { + val keyMap = KeyMap() + keyMap.bindKey(UP, KeyMap.key(terminal, InfoCmp.Capability.key_up)) + keyMap.bindKey(DOWN, KeyMap.key(terminal, InfoCmp.Capability.key_down)) + keyMap.bindKey(LEFT, KeyMap.key(terminal, InfoCmp.Capability.key_left)) + keyMap.bindKey(RIGHT, KeyMap.key(terminal, InfoCmp.Capability.key_right)) + keyMap.bind(UP, "\u001B[A", "k", "w") + keyMap.bind(DOWN, "\u001B[B", "j", "s") + keyMap.bind(LEFT, "\u001B[D", "\u001B[5~", "h", "a") + keyMap.bind(RIGHT, "\u001B[C", "\u001B[6~", "l", "d") + keyMap.bind(ENTER, "\r", "\n") + keyMap.bindKey(ENTER, KeyMap.key(terminal, InfoCmp.Capability.key_enter)) + keyMap.bind(CANCEL, "\u001B", "q", KeyMap.ctrl('C')) + return keyMap + } + + private fun clearScreen(terminal: Terminal) { + val cleared = terminal.puts(InfoCmp.Capability.clear_screen) + val homed = terminal.puts(InfoCmp.Capability.cursor_home) + if (!cleared || !homed) { + terminal.writer().print("\u001B[2J\u001B[H") + } + } + + private fun KeyMap.bindKey(action: String, sequence: String?) { + if (!sequence.isNullOrEmpty()) { + bind(action, sequence) + } + } +} diff --git a/src/main/kotlin/file/YamlHelper.kt b/src/main/kotlin/file/YamlHelper.kt index 1cbdd27..0f3401a 100644 --- a/src/main/kotlin/file/YamlHelper.kt +++ b/src/main/kotlin/file/YamlHelper.kt @@ -64,6 +64,9 @@ object YamlHelper { if (configData.projectName.isBlank()) { configData.projectName = sourcePath?.parent?.fileName?.toString() ?: "unnamed" } + if (configData.wc3Patch != null) { + configData.wc3Patch = CoreJassProvider.normalizePatchInput(configData.wc3Patch) + } return configData } diff --git a/src/main/resources/wbschema.json b/src/main/resources/wbschema.json index c382a16..04ed87b 100644 --- a/src/main/resources/wbschema.json +++ b/src/main/resources/wbschema.json @@ -15,9 +15,40 @@ "enum": ["LUA", "JASS"] }, "wc3Patch": { - "description": "The WC3 patch target for this project. Affects which stdlib branch is used. Default: REFORGED.", + "description": "The WC3 patch target for this project. Grill maps this friendly version to the matching Luashine/jass-history war3extract folder. Default: v1.36.", "type": "string", - "enum": ["REFORGED", "PRE_129"] + "enum": [ + "v1.36", "v1.35", "v1.34", "v1.33", "v1.32", "v1.31", "v1.30", "v1.29", "v1.28", + "v1.27b", "v1.27a", "v1.26a", "v1.25b", + "v1.24e", "v1.24d", "v1.24c", "v1.24b", "v1.24a", + "v1.23a", "v1.22a", + "v1.21b", "v1.21a", "v1.21", + "v1.20e", "v1.20d", "v1.20c", "v1.20b", "v1.20a", "v1.20", + "v1.19b", "v1.19a", "v1.18a", "v1.17a", "v1.16a", + "v1.15", "v1.14b", "v1.14", "v1.13b", "v1.13", + "v1.12", "v1.11", "v1.10", "v1.07", + "v1.06", "v1.05", "v1.04", "v1.03", "v1.02a", "v1.02", "v1.01b", "v1.01", "v1.00" + ], + "markdownEnumDescriptions": [ + "Latest Reforged / WC3 2.x core JASS. Uses Reforged-v1.36.1.20719-w3-51d40ee.", + "Reforged core JASS. Uses Reforged-v1.35.0.20093-w3-5ec1b77.", + "Reforged core JASS. Uses Reforged-v1.34.0.19632-w3-31590bf.", + "Reforged core JASS. Uses Reforged-v1.33.0.19378-w3-e94d62c.", + "Reforged core JASS. Uses Reforged-v1.32.10.19202.", + "Latest classic TFT core JASS. Uses TFT-v1.31.1.12173.", + "Classic TFT core JASS. Uses TFT-v1.30.4.11274.", + "Classic TFT core JASS. Uses TFT-v1.29.2.9231.", + "Legacy pre-1.29 core JASS. Uses TFT-v1.28.2.7395.", + "Classic TFT core JASS.", "Classic TFT core JASS.", "Classic TFT core JASS.", "Classic TFT core JASS.", + "Classic TFT core JASS.", "Classic TFT core JASS.", "Classic TFT core JASS.", "Classic TFT core JASS.", "Classic TFT core JASS.", + "Classic TFT core JASS.", "Classic TFT core JASS.", + "Classic TFT core JASS.", "Classic TFT core JASS.", "Classic TFT core JASS.", + "Classic TFT core JASS.", "Classic TFT core JASS.", "Classic TFT core JASS.", "Classic TFT core JASS.", "Classic TFT core JASS.", "Classic ROC beta core JASS.", + "Classic TFT core JASS.", "Classic TFT core JASS.", "Classic TFT core JASS.", "Classic TFT core JASS.", "Classic TFT core JASS.", + "Classic TFT core JASS.", "Classic TFT core JASS.", "Classic TFT core JASS.", "Classic TFT core JASS.", "Classic TFT core JASS.", + "Classic TFT core JASS.", "Classic TFT core JASS.", "Classic TFT core JASS.", "Classic TFT core JASS.", + "Classic ROC core JASS.", "Classic ROC core JASS.", "Classic ROC core JASS.", "Classic ROC core JASS.", "Classic ROC core JASS.", "Classic ROC core JASS.", "Classic ROC core JASS.", "Classic ROC core JASS.", "Classic ROC core JASS." + ] }, "dependencies": { "description": "Git repository urls of this project's dependencies", diff --git a/src/test/kotlin/GenerateTests.kt b/src/test/kotlin/GenerateTests.kt index f96f954..29d4de1 100644 --- a/src/test/kotlin/GenerateTests.kt +++ b/src/test/kotlin/GenerateTests.kt @@ -1,6 +1,6 @@ import config.ScriptMode -import config.Wc3Patch import file.CLICommand +import file.CoreJassProvider import file.ExitHandler import file.SetupApp import file.SetupMain @@ -34,7 +34,7 @@ class GenerateTests { Assert.assertEquals(setup.command, CLICommand.GENERATE) Assert.assertEquals(setup.commandArg, "myproject") Assert.assertEquals(setup.scriptMode, ScriptMode.LUA) - Assert.assertEquals(setup.wc3Patch, Wc3Patch.REFORGED) + Assert.assertEquals(setup.wc3Patch, CoreJassProvider.DEFAULT_PATCH) Assert.assertFalse(setup.addAgents) Assert.assertFalse(setup.addGithubWorkflow) } @@ -44,7 +44,7 @@ class GenerateTests { val setup = SetupMain() setup.parseArgs(listOf("generate", "myproject", "--script-mode", "jass")) Assert.assertEquals(setup.scriptMode, ScriptMode.JASS) - Assert.assertEquals(setup.wc3Patch, Wc3Patch.REFORGED) + Assert.assertEquals(setup.wc3Patch, CoreJassProvider.DEFAULT_PATCH) } @Test(priority = 10) @@ -52,7 +52,59 @@ class GenerateTests { val setup = SetupMain() setup.parseArgs(listOf("generate", "myproject", "--wc3-patch", "pre1.29")) Assert.assertEquals(setup.scriptMode, ScriptMode.LUA) - Assert.assertEquals(setup.wc3Patch, Wc3Patch.PRE_129) + Assert.assertEquals(setup.wc3Patch, CoreJassProvider.PRE_129_PATCH) + } + + @Test(priority = 10) + fun testWc3PatchJassHistoryVersionFlag() { + val setup = SetupMain() + setup.parseArgs(listOf("generate", "myproject", "--wc3-patch", "Reforged-v1.36.1.20719-w3-51d40ee")) + Assert.assertEquals(setup.wc3Patch, "v1.36") + + val letterPatch = SetupMain() + letterPatch.parseArgs(listOf("generate", "myproject", "--wc3-patch", "1.27b")) + Assert.assertEquals(letterPatch.wc3Patch, "v1.27b") + } + + @Test(priority = 10) + fun testPatchAliasNormalizationAndLegacyDetection() { + Assert.assertEquals(CoreJassProvider.normalizePatchInput("reforged"), CoreJassProvider.DEFAULT_PATCH) + Assert.assertEquals(CoreJassProvider.normalizePatchInput("pre1.29"), CoreJassProvider.PRE_129_PATCH) + Assert.assertFalse(CoreJassProvider.isSupportedPatch("v9.99")) + Assert.assertTrue(CoreJassProvider.isPre129Patch("TFT-v1.28.2.7395")) + Assert.assertFalse(CoreJassProvider.isPre129Patch("TFT-v1.31.1.12173")) + Assert.assertEquals( + CoreJassProvider.jassHistoryFolderForPatch("v1.36"), + "Reforged-v1.36.1.20719-w3-51d40ee" + ) + Assert.assertEquals( + CoreJassProvider.describePatch("v1.36"), + "v1.36 (latest Reforged / WC3 2.x core JASS)" + ) + } + + @Test(priority = 10) + fun testInstallPatchSelectionRejectsUnsupportedFreeText() { + val answers = java.util.ArrayDeque(listOf("totally-not-a-patch", "2")) + val prevPrompt = SetupApp.installPatchPrompt + try { + SetupApp.installPatchPrompt = { _, _ -> answers.removeFirst() } + Assert.assertEquals(SetupApp.selectPatchVersionForInstall(), "v1.31") + } finally { + SetupApp.installPatchPrompt = prevPrompt + } + } + + @Test(priority = 10) + fun testInstallPatchSelectionRequiresBrowseForNonRecommendedVersions() { + val answers = java.util.ArrayDeque(listOf("v1.32", "more", "v1.32")) + val prevPrompt = SetupApp.installPatchPrompt + try { + SetupApp.installPatchPrompt = { _, _ -> answers.removeFirst() } + Assert.assertEquals(SetupApp.selectPatchVersionForInstall(), "v1.32") + } finally { + SetupApp.installPatchPrompt = prevPrompt + } } @Test(priority = 10) @@ -68,7 +120,7 @@ class GenerateTests { ) ) Assert.assertEquals(setup.scriptMode, ScriptMode.JASS) - Assert.assertEquals(setup.wc3Patch, Wc3Patch.PRE_129) + Assert.assertEquals(setup.wc3Patch, CoreJassProvider.PRE_129_PATCH) Assert.assertTrue(setup.addAgents) Assert.assertTrue(setup.addGithubWorkflow) } @@ -129,11 +181,51 @@ class GenerateTests { Assert.assertEquals(setup.commandArg, "wizardproject") Assert.assertEquals(setup.scriptMode, ScriptMode.JASS) - Assert.assertEquals(setup.wc3Patch, Wc3Patch.PRE_129) + Assert.assertEquals(setup.wc3Patch, CoreJassProvider.PRE_129_PATCH) Assert.assertTrue(setup.addAgents) Assert.assertTrue(setup.addGithubWorkflow) } + @Test(priority = 10) + fun testGenerateWizardRejectsUnsupportedScriptModeAndPatchInput() { + val setup = SetupMain() + setup.parseArgs(listOf("generate")) + val answers = java.util.ArrayDeque(listOf("wizardproject", "t", "jass", "t", "2", "n", "n")) + val prevPrompt = SetupApp.generatePrompt + try { + SetupApp.generatePrompt = { _, _ -> answers.removeFirst() } + Assert.assertTrue(SetupApp.prepareGenerate(setup)) + } finally { + SetupApp.generatePrompt = prevPrompt + } + + Assert.assertEquals(setup.commandArg, "wizardproject") + Assert.assertEquals(setup.scriptMode, ScriptMode.JASS) + Assert.assertEquals(setup.wc3Patch, "v1.31") + Assert.assertFalse(setup.addAgents) + Assert.assertFalse(setup.addGithubWorkflow) + } + + @Test(priority = 10) + fun testGenerateWizardPreservesCliPatchDefault() { + val setup = SetupMain() + setup.parseArgs(listOf("generate", "--wc3-patch", "pre1.29")) + val answers = java.util.ArrayDeque(listOf("wizardproject", "", "", "n", "n")) + val prevPrompt = SetupApp.generatePrompt + try { + SetupApp.generatePrompt = { _, _ -> answers.removeFirst() } + Assert.assertTrue(SetupApp.prepareGenerate(setup)) + } finally { + SetupApp.generatePrompt = prevPrompt + } + + Assert.assertEquals(setup.commandArg, "wizardproject") + Assert.assertEquals(setup.scriptMode, ScriptMode.LUA) + Assert.assertEquals(setup.wc3Patch, CoreJassProvider.PRE_129_PATCH) + Assert.assertFalse(setup.addAgents) + Assert.assertFalse(setup.addGithubWorkflow) + } + @Test(priority = 10) fun testGenerateWithoutNameReturnsWithoutGeneratingWhenPromptCannotReadName() { val setup = SetupMain() @@ -179,4 +271,110 @@ class GenerateTests { } } } + + @Test(priority = 10) + fun testCoreJassFilesAreEmittedToBuildDir() { + val tmpDir = Files.createTempDirectory("grill-core-jass-test") + try { + SetupApp.ensureCoreJassFiles(tmpDir, CoreJassProvider.DEFAULT_PATCH) + + val common = tmpDir.resolve("_build/common.j") + val blizzard = tmpDir.resolve("_build/blizzard.j") + Assert.assertTrue(Files.exists(common), "common.j should be emitted directly into _build") + Assert.assertTrue(Files.exists(blizzard), "blizzard.j should be emitted directly into _build") + Assert.assertTrue(Files.readString(common).contains("native ConvertRace")) + Assert.assertTrue(Files.readString(blizzard).contains("Blizzard.j")) + } finally { + Files.walk(tmpDir).sorted(Comparator.reverseOrder()).forEach { + try { + Files.deleteIfExists(it) + } catch (_: Exception) { + } + } + } + } + + @Test(priority = 10) + fun testCoreJassFilesFollowConfiguredPatch() { + val tmpDir = Files.createTempDirectory("grill-core-jass-patch-test") + try { + SetupApp.ensureCoreJassFiles(tmpDir, CoreJassProvider.DEFAULT_PATCH) + val reforgedCommonSize = Files.size(tmpDir.resolve("_build/common.j")) + + SetupApp.ensureCoreJassFiles(tmpDir, CoreJassProvider.PRE_129_PATCH) + val pre129CommonSize = Files.size(tmpDir.resolve("_build/common.j")) + + Assert.assertNotEquals( + pre129CommonSize, + reforgedCommonSize, + "install/generate should refresh _build/common.j when wc3Patch changes" + ) + } finally { + Files.walk(tmpDir).sorted(Comparator.reverseOrder()).forEach { + try { + Files.deleteIfExists(it) + } catch (_: Exception) { + } + } + } + } + + @Test(priority = 10) + fun testExistingCoreJassWithoutProvenanceIsPreserved() { + val tmpDir = Files.createTempDirectory("grill-core-jass-preserve-test") + try { + val buildDir = tmpDir.resolve("_build") + Files.createDirectories(buildDir) + val customCommon = "// custom common.j\n" + val customBlizzard = "// custom blizzard.j\n" + Files.writeString(buildDir.resolve("common.j"), customCommon) + Files.writeString(buildDir.resolve("blizzard.j"), customBlizzard) + + SetupApp.ensureCoreJassFiles(tmpDir, CoreJassProvider.DEFAULT_PATCH) + + Assert.assertEquals(Files.readString(buildDir.resolve("common.j")), customCommon) + Assert.assertEquals(Files.readString(buildDir.resolve("blizzard.j")), customBlizzard) + Assert.assertFalse( + Files.exists(buildDir.resolve("core-jass.provenance")), + "Grill must not claim ownership of pre-existing project-local core JASS files" + ) + } finally { + Files.walk(tmpDir).sorted(Comparator.reverseOrder()).forEach { + try { + Files.deleteIfExists(it) + } catch (_: Exception) { + } + } + } + } + + @Test(priority = 10) + fun testMatchingProvenanceUsesCachedCoreJass() { + val tmpDir = Files.createTempDirectory("grill-core-jass-cache-test") + try { + val buildDir = tmpDir.resolve("_build") + Files.createDirectories(buildDir) + val cachedCommon = "// cached common.j\n" + "x".repeat(2048) + val cachedBlizzard = "// cached blizzard.j\n" + "y".repeat(2048) + Files.writeString(buildDir.resolve("common.j"), cachedCommon) + Files.writeString(buildDir.resolve("blizzard.j"), cachedBlizzard) + Files.writeString( + buildDir.resolve("core-jass.provenance"), + "wc3Patch: ${CoreJassProvider.DEFAULT_PATCH}\n" + + "jassHistoryFolder: ${CoreJassProvider.jassHistoryFolderForPatch(CoreJassProvider.DEFAULT_PATCH)}\n" + ) + + SetupApp.ensureCoreJassFiles(tmpDir, CoreJassProvider.DEFAULT_PATCH) + + Assert.assertEquals(Files.readString(buildDir.resolve("common.j")), cachedCommon) + Assert.assertEquals(Files.readString(buildDir.resolve("blizzard.j")), cachedBlizzard) + } finally { + Files.walk(tmpDir).sorted(Comparator.reverseOrder()).forEach { + try { + Files.deleteIfExists(it) + } catch (_: Exception) { + } + } + } + } } diff --git a/src/test/kotlin/YamlHelperTests.kt b/src/test/kotlin/YamlHelperTests.kt index f779c3e..83bea05 100644 --- a/src/test/kotlin/YamlHelperTests.kt +++ b/src/test/kotlin/YamlHelperTests.kt @@ -25,4 +25,17 @@ class YamlHelperTests { Assert.assertTrue(Files.exists(buildFile)) Assert.assertTrue(Files.exists(dir.resolve("wurst.build.bak"))) } + + @Test + fun testWc3PatchVersionNormalizesToSchemaValue() { + val dumped = YamlHelper.dumpProjectConfig( + WurstProjectConfigData(projectName = "versioned", wc3Patch = "Reforged-v1.36.1.20719-w3-51d40ee") + ) + val dir = Files.createTempDirectory("wurstsetup-yaml-version-test") + val buildFile = dir.resolve("wurst.build") + Files.writeString(buildFile, dumped) + + val loaded = YamlHelper.loadProjectConfig(buildFile) + Assert.assertEquals(loaded.wc3Patch, "v1.36") + } }