From 07218d74c6a696bbb343fcabb7a65976bbcb6013 Mon Sep 17 00:00:00 2001 From: Frotty Date: Sun, 13 Sep 2026 01:14:42 +0200 Subject: [PATCH 1/7] Add WC3 patch alignment workflow --- AGENTS.md | 2 + README.md | 18 ++++ src/main/kotlin/config/WurstProjectConfig.kt | 16 +++ src/main/kotlin/file/CLICommand.kt | 3 +- src/main/kotlin/file/CoreJassProvider.kt | 15 +++ src/main/kotlin/file/SetupApp.kt | 96 +++++++++++++++++- src/main/kotlin/file/Wc3ClientDetector.kt | 55 ++++++++-- src/test/kotlin/GenerateTests.kt | 6 +- src/test/kotlin/PatchAlignmentTests.kt | 100 +++++++++++++++++++ src/test/kotlin/Wc3ClientDetectorTests.kt | 55 ++++++++++ 10 files changed, 353 insertions(+), 13 deletions(-) create mode 100644 src/test/kotlin/PatchAlignmentTests.kt diff --git a/AGENTS.md b/AGENTS.md index 76bdda9..b9f449a 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -48,6 +48,8 @@ This repo builds the Grill CLI and project setup tooling. The generated map-proj - Build/typecheck should not require parsing the installed Warcraft executable when `wc3Patch` is pinned. - Run/launch is different: the selected WC3 executable controls launch arguments. If the client family and project patch target differ, warn and allow choosing another WC3 folder. +- `grill patch` is a read-only exact-version check. `grill patch align` is the explicit mutation boundary: update `wc3Patch`, the official stdlib branch, and Grill-owned core JASS together, and preserve custom stdlib forks. +- Exact Reforged patch detection comes from the active Warcraft III row in Blizzard's `.build.info`. If it is missing or cannot map to a supported target, alignment must leave the project unchanged. - Keep compiler-facing patch behavior tested in the WurstScript repo as well; Grill and compiler can diverge if only one side is tested. ## Test Commands diff --git a/README.md b/README.md index 343d051..5c00e55 100644 --- a/README.md +++ b/README.md @@ -70,6 +70,24 @@ The command exits with code `0` when dependencies are up to date and `1` when up > grill outdated ``` +### Aligning with the installed Warcraft III patch + +Use `patch` to compare the project target with the exact installed Warcraft III version. This is read-only: + +```cmd +> grill patch +``` + +When Grill reports a mismatch, migrate the project configuration, official stdlib branch, and cached core JASS together: + +```cmd +> grill patch align +``` + +Grill reads the configured VS Code game path first and falls back to automatic detection. Pass `--wc3-path ` to select a different installation. Alignment creates `wurst.build.bak` and makes no changes if an exact supported client patch cannot be detected. + +`grill install` also keeps the official stdlib dependency pinned to the branch required by the project's existing `wc3Patch`, and only suggests migration when it detects a different client patch. + ### Building the project diff --git a/src/main/kotlin/config/WurstProjectConfig.kt b/src/main/kotlin/config/WurstProjectConfig.kt index 3d7d88b..6cfca27 100644 --- a/src/main/kotlin/config/WurstProjectConfig.kt +++ b/src/main/kotlin/config/WurstProjectConfig.kt @@ -145,6 +145,22 @@ object WurstProjectConfig { Files.write(projectRoot.resolve(CONFIG_FILE_NAME), projectYaml.toByteArray()) } + fun configuredGamePath(projectRoot: Path): Path? { + val settings = projectRoot.resolve(".vscode").resolve("settings.json") + if (!Files.isRegularFile(settings)) { + return null + } + return try { + MAPPER.readTree(Files.readString(settings)) + ?.get("wurst.wc3path") + ?.asText() + ?.takeIf(String::isNotBlank) + ?.let(Paths::get) + } catch (_: Exception) { + null + } + } + @Throws(IOException::class) private fun setupVSCode(projectRoot: Path?, gamePath: Path?) { diff --git a/src/main/kotlin/file/CLICommand.kt b/src/main/kotlin/file/CLICommand.kt index 2067966..9b017b5 100644 --- a/src/main/kotlin/file/CLICommand.kt +++ b/src/main/kotlin/file/CLICommand.kt @@ -10,7 +10,8 @@ enum class CLICommand { GENERATE, TEST, TYPECHECK, - OUTDATED, + OUTDATED, + PATCH, BUILD, EXPORTOBJECTS, SELF_UPDATE diff --git a/src/main/kotlin/file/CoreJassProvider.kt b/src/main/kotlin/file/CoreJassProvider.kt index 793717f..b457e84 100644 --- a/src/main/kotlin/file/CoreJassProvider.kt +++ b/src/main/kotlin/file/CoreJassProvider.kt @@ -165,6 +165,21 @@ object CoreJassProvider { return withPrefix } + fun patchTargetForClientVersion(version: String?): String? { + val parts = version?.trim()?.split('.') ?: return null + if (parts.size < 2 || parts[0].toIntOrNull() == null || parts[1].toIntOrNull() == null) { + return null + } + val target = "v${parts[0].toInt()}.${parts[1].toInt()}" + return target.takeIf(PATCH_TO_JASS_HISTORY_FOLDER::containsKey) + } + + fun patchLine(patch: String?): String? { + val normalized = normalizePatchInput(patch) + val version = Wc3PatchTarget.parse(normalized).orElse(null)?.gameVersion() ?: return null + return patchTargetForClientVersion(version) + } + fun isPre129Patch(input: String?): Boolean { val patch = normalizePatchInput(input) return Wc3PatchTarget.parse(patch) diff --git a/src/main/kotlin/file/SetupApp.kt b/src/main/kotlin/file/SetupApp.kt index 87fa2d5..d30185a 100644 --- a/src/main/kotlin/file/SetupApp.kt +++ b/src/main/kotlin/file/SetupApp.kt @@ -7,6 +7,7 @@ import config.WurstProjectBuildMapData import config.WurstProjectConfigData import config.newProjectConfig import config.withAddedDependency +import config.withDependencies import config.withRemovedDependency import config.withWc3Patch import global.InstallationManager @@ -21,6 +22,7 @@ import java.nio.charset.StandardCharsets 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.concurrent.TimeUnit import javax.swing.JOptionPane @@ -155,6 +157,7 @@ object SetupApp { | test [filter] Run unit tests, optionally filtered by package/function name | typecheck Typecheck the project without building a map | outdated Check whether project dependencies are up to date + | patch [align] Compare project/client patches; align config and stdlib on request | build Build the project using the given map archive or folder | exportobjects Export object editor data to Wurst source | @@ -173,12 +176,16 @@ object SetupApp { | --with-agents / --no-agents Include AGENTS.md (default: no) | --with-ci / --no-ci Include GitHub Actions workflow (default: no) | --with-dep Add a curated dependency (repeatable; ids: ${CuratedDependencies.ids.joinToString(", ")}) + | + |Patch options: + | --wc3-path Warcraft III install folder to inspect """.trimMargin()) } setup.command == CLICommand.INSTALL -> { if (setup.commandArg.isBlank()) { if (configData != null) { configData = ensureProjectPatchRecorded(configData) + suggestPatchAlignment(configData) handleUpdateProject(configData) } else { missingProject() @@ -191,6 +198,7 @@ object SetupApp { if (configData != null) { configData = handleInstallDep(configData) configData = ensureProjectPatchRecorded(configData) + suggestPatchAlignment(configData) WurstProjectConfig.saveProjectConfig(setup.projectRoot, configData) handleUpdateProject(configData) } else { @@ -261,6 +269,13 @@ object SetupApp { } checkProjectOutdated(configData) } + setup.command == CLICommand.PATCH -> { + if (configData == null) { + missingProject() + } else { + handlePatchAlignment(configData) + } + } setup.command == CLICommand.BUILD -> { progress("🔨 Building project...") val mapArg = if (setup.commandArg.isBlank()) { @@ -741,8 +756,82 @@ object SetupApp { return when { CoreJassProvider.isPre124(wc3Patch) -> "https://github.com/wurstscript/wurstStdlib2:pre1.24" CoreJassProvider.isPre129Patch(wc3Patch) -> "https://github.com/wurstscript/wurstStdlib2:pre1.29" - else -> "https://github.com/wurstscript/wurstStdlib2" + CoreJassProvider.patchLine(wc3Patch) == "v3.0" -> "https://github.com/wurstscript/wurstStdlib2" + else -> "https://github.com/wurstscript/wurstStdlib2:v2.0" + } + } + + internal fun alignedProjectConfig(configData: WurstProjectConfigData, patchTarget: String): WurstProjectConfigData { + return alignOfficialStdlibDependency(configData.withWc3Patch(patchTarget), patchTarget) + } + + internal fun alignOfficialStdlibDependency(configData: WurstProjectConfigData, patchTarget: String): WurstProjectConfigData { + val expectedStdlib = stdlibDependencyForPatch(patchTarget) + val dependencies = configData.dependencies.map { dependency -> + if (isOfficialStdlibDependency(dependency)) expectedStdlib else dependency + }.distinct() + return configData.withDependencies(dependencies) + } + + private fun isOfficialStdlibDependency(dependency: String): Boolean { + val prefix = "https://github.com/wurstscript/wurstStdlib2" + if (!dependency.startsWith(prefix, ignoreCase = true)) { + return false + } + val suffix = dependency.substring(prefix.length) + return suffix.isBlank() || suffix.startsWith(":") + } + + private fun handlePatchAlignment(configData: WurstProjectConfigData) { + val action = setup.commandArg.trim().lowercase() + if (action.isNotBlank() && action != "align") { + fail("Unknown patch action: ${setup.commandArg}. Use `grill patch` or `grill patch align`.") + ExitHandler.exit(1) + return } + + val configuredPath = WurstProjectConfig.configuredGamePath(setup.projectRoot) + val gameRoot = setup.gamePath ?: configuredPath ?: Wc3ClientDetector.detectGameRoot() + val clientInfo = Wc3ClientDetector.inspectGameRoot(gameRoot) + if (clientInfo == null) { + fail("No Warcraft III installation was found. Use `grill patch --wc3-path `.") + ExitHandler.exit(1) + return + } + + val detectedPatch = clientInfo.patchTarget + val currentPatch = CoreJassProvider.patchLine(configData.wc3Patch) + log.info("Project patch: ${currentPatch ?: configData.wc3Patch ?: "not configured"}") + log.info("Detected client: ${Wc3ClientDetector.describe(clientInfo)}") + if (detectedPatch == null) { + fail("The exact Warcraft III patch could not be mapped to a supported target. No files were changed.") + ExitHandler.exit(1) + return + } + + val alignedConfig = alignedProjectConfig(configData, detectedPatch) + if (alignedConfig == configData) { + pass("Project is already aligned with Warcraft III $detectedPatch.") + return + } + if (action != "align") { + log.info("Alignment available: ${currentPatch ?: "unconfigured"} -> $detectedPatch") + log.info("Run `grill patch align` to update wurst.build, stdlib, and core JASS.") + return + } + + val buildFile = setup.projectRoot.resolve(CONFIG_FILE_NAME) + Files.copy(buildFile, buildFile.resolveSibling("$CONFIG_FILE_NAME.bak"), StandardCopyOption.REPLACE_EXISTING) + ensureCoreJassFiles(setup.projectRoot, detectedPatch) + WurstProjectConfig.handleUpdate(setup.projectRoot, clientInfo.root, alignedConfig) + pass("Aligned project with Warcraft III $detectedPatch. Previous config: $CONFIG_FILE_NAME.bak") + } + + private fun suggestPatchAlignment(configData: WurstProjectConfigData) { + val configuredPath = WurstProjectConfig.configuredGamePath(setup.projectRoot) + val gameRoot = setup.gamePath ?: configuredPath ?: Wc3ClientDetector.detectGameRoot() + val clientInfo = Wc3ClientDetector.inspectGameRoot(gameRoot) ?: return + Wc3ClientDetector.mismatchMessage(configData.wc3Patch, clientInfo)?.let(log::warn) } internal fun generatedBuildMapData(projectName: String): WurstProjectBuildMapData { @@ -805,15 +894,16 @@ object SetupApp { if (currentPatch.isNullOrBlank()) { val selectedPatch = selectPatchVersionForInstall() log.info("WC3 patch recorded in wurst.build: $selectedPatch") - return configData.withWc3Patch(selectedPatch) + return alignOfficialStdlibDependency(configData.withWc3Patch(selectedPatch), selectedPatch) } val normalizedPatch = CoreJassProvider.normalizePatchInput(currentPatch) - return if (normalizedPatch != currentPatch) { + val normalizedConfig = if (normalizedPatch != currentPatch) { configData.withWc3Patch(normalizedPatch) } else { configData } + return alignOfficialStdlibDependency(normalizedConfig, normalizedPatch) } internal fun selectPatchVersionForInstall(): String { diff --git a/src/main/kotlin/file/Wc3ClientDetector.kt b/src/main/kotlin/file/Wc3ClientDetector.kt index cae1cb6..a6787bc 100644 --- a/src/main/kotlin/file/Wc3ClientDetector.kt +++ b/src/main/kotlin/file/Wc3ClientDetector.kt @@ -17,11 +17,15 @@ object Wc3ClientDetector { val root: Path, val executable: Path, val kind: ClientKind?, + val version: String?, + val patchTarget: String?, ) private val exeCandidates = listOf( Paths.get("_retail_", "x86_64", "Warcraft III.exe"), Paths.get("_retail_", "x86", "Warcraft III.exe"), + Paths.get("_ptr_", "x86_64", "Warcraft III.exe"), + Paths.get("_ptr_", "x86", "Warcraft III.exe"), Paths.get("x86_64", "Warcraft III.exe"), Paths.get("x86", "Warcraft III.exe"), Paths.get("Warcraft III.exe"), @@ -42,12 +46,15 @@ object Wc3ClientDetector { } val normalizedRoot = root.toAbsolutePath().normalize() val executable = findExecutable(normalizedRoot) ?: return null - val installRoot = if (Files.isRegularFile(normalizedRoot)) { - installationRootForExecutable(executable) - } else { - normalizedRoot - } - return ClientInfo(installRoot, executable, classifyExecutable(executable)) + val installRoot = installationRootForExecutable(executable) + val version = readBuildInfoVersion(installRoot) + return ClientInfo( + installRoot, + executable, + classifyExecutable(executable), + version, + CoreJassProvider.patchTargetForClientVersion(version) + ) } fun describe(info: ClientInfo?): String { @@ -55,7 +62,8 @@ object Wc3ClientDetector { return "not found" } val kind = info.kind?.let { describeKind(it) } ?: "unknown patch family" - return "${info.root} ($kind)" + val version = info.version?.let { ", version $it" }.orEmpty() + return "${info.root} ($kind$version)" } fun projectKind(patch: String?): ClientKind? { @@ -70,6 +78,11 @@ object Wc3ClientDetector { fun mismatchMessage(projectPatch: String?, clientInfo: ClientInfo?): String? { val projectKind = projectKind(projectPatch) ?: return null val clientKind = clientInfo?.kind ?: return null + val projectPatchTarget = CoreJassProvider.patchLine(projectPatch) + val clientPatchTarget = clientInfo.patchTarget + if (projectPatchTarget != null && clientPatchTarget != null && projectPatchTarget != clientPatchTarget) { + return "Selected Warcraft III client is $clientPatchTarget (${clientInfo.version}), but the project targets $projectPatchTarget. Run `grill patch align` to migrate the project." + } if (projectKind == clientKind) { return null } @@ -121,6 +134,34 @@ object Wc3ClientDetector { return parent } + private fun readBuildInfoVersion(root: Path): String? { + val buildInfo = root.resolve(".build.info") + if (!Files.isRegularFile(buildInfo)) { + return null + } + return try { + val lines = Files.readAllLines(buildInfo).filter(String::isNotBlank) + val headers = lines.firstOrNull()?.split('|')?.map { it.substringBefore('!') } ?: return null + val versionIndex = headers.indexOf("Version") + val productIndex = headers.indexOf("Product") + val activeIndex = headers.indexOf("Active") + if (versionIndex < 0) return null + + lines.drop(1) + .map { it.split('|') } + .firstOrNull { values -> + values.size > versionIndex && + (productIndex < 0 || values.getOrNull(productIndex).equals("w3", ignoreCase = true)) && + (activeIndex < 0 || values.getOrNull(activeIndex) == "1") + } + ?.getOrNull(versionIndex) + ?.trim() + ?.takeIf(String::isNotBlank) + } catch (_: Exception) { + null + } + } + private fun describeKind(kind: ClientKind): String { return when (kind) { ClientKind.PRE_129 -> "pre-1.29" diff --git a/src/test/kotlin/GenerateTests.kt b/src/test/kotlin/GenerateTests.kt index d284117..5f98907 100644 --- a/src/test/kotlin/GenerateTests.kt +++ b/src/test/kotlin/GenerateTests.kt @@ -148,6 +148,7 @@ class GenerateTests { fun testStdlibDependencyFollowsPatchEra() { val pre124Stdlib = "https://github.com/wurstscript/wurstStdlib2:pre1.24" val legacyStdlib = "https://github.com/wurstscript/wurstStdlib2:pre1.29" + val preV3Stdlib = "https://github.com/wurstscript/wurstStdlib2:v2.0" val currentStdlib = "https://github.com/wurstscript/wurstStdlib2" for (patch in CoreJassProvider.supportedPatches) { @@ -155,7 +156,8 @@ class GenerateTests { val expected = when { minor != null && minor < 24 -> pre124Stdlib minor != null && minor < 29 -> legacyStdlib - else -> currentStdlib + patch == "v3.0" -> currentStdlib + else -> preV3Stdlib } Assert.assertEquals(SetupApp.stdlibDependencyForPatch(patch), expected, "stdlib dependency for $patch") } @@ -163,7 +165,7 @@ class GenerateTests { Assert.assertEquals(SetupApp.stdlibDependencyForPatch("v1.23a"), pre124Stdlib) Assert.assertEquals(SetupApp.stdlibDependencyForPatch("TFT-v1.27b-ru"), legacyStdlib) Assert.assertEquals(SetupApp.stdlibDependencyForPatch("pre1.29"), legacyStdlib) - Assert.assertEquals(SetupApp.stdlibDependencyForPatch("v1.29"), currentStdlib) + Assert.assertEquals(SetupApp.stdlibDependencyForPatch("v1.29"), preV3Stdlib) } @Test(priority = 10) diff --git a/src/test/kotlin/PatchAlignmentTests.kt b/src/test/kotlin/PatchAlignmentTests.kt new file mode 100644 index 0000000..ffdfb2a --- /dev/null +++ b/src/test/kotlin/PatchAlignmentTests.kt @@ -0,0 +1,100 @@ +import config.newProjectConfig +import file.CoreJassProvider +import file.CLICommand +import file.SetupApp +import file.SetupMain +import org.testng.Assert +import org.testng.annotations.Test +import java.nio.file.Paths + +class PatchAlignmentTests { + @Test + fun testParsesPatchAlignWithExplicitGamePath() { + val setup = SetupMain() + val gamePath = Paths.get("C:\\Games\\Warcraft III") + + setup.parseArgs(listOf("patch", "align", "--wc3-path", gamePath.toString())) + + Assert.assertEquals(setup.command, CLICommand.PATCH) + Assert.assertEquals(setup.commandArg, "align") + Assert.assertEquals(setup.gamePath, gamePath) + } + + @Test + fun testMapsClientVersionsToSupportedPatchLines() { + Assert.assertEquals(CoreJassProvider.patchTargetForClientVersion("3.0.0.24268"), "v3.0") + Assert.assertEquals(CoreJassProvider.patchTargetForClientVersion("2.0.4.23745"), "v2.0") + Assert.assertEquals(CoreJassProvider.patchTargetForClientVersion("1.36.1.20719"), "v1.36") + Assert.assertNull(CoreJassProvider.patchTargetForClientVersion("4.0.0.1")) + } + + @Test + fun testV2ProjectsUseTheMaintenanceStdlibBranch() { + Assert.assertEquals( + SetupApp.stdlibDependencyForPatch("v2.0"), + "https://github.com/wurstscript/wurstStdlib2:v2.0" + ) + Assert.assertEquals( + SetupApp.stdlibDependencyForPatch("v3.0"), + "https://github.com/wurstscript/wurstStdlib2" + ) + Assert.assertEquals( + SetupApp.stdlibDependencyForPatch("v1.36"), + "https://github.com/wurstscript/wurstStdlib2:v2.0" + ) + } + + @Test + fun testAlignmentUpdatesPatchAndOfficialStdlibTogether() { + val config = newProjectConfig( + projectName = "migration-test", + dependencies = listOf( + "https://github.com/wurstscript/wurstStdlib2:v2.0", + "https://github.com/example/custom-library" + ), + wc3Patch = "v2.0" + ) + + val aligned = SetupApp.alignedProjectConfig(config, "v3.0") + + Assert.assertEquals(aligned.wc3Patch, "v3.0") + Assert.assertEquals( + aligned.dependencies, + listOf( + "https://github.com/wurstscript/wurstStdlib2", + "https://github.com/example/custom-library" + ) + ) + } + + @Test + fun testAlignmentDoesNotRewriteCustomStdlibForks() { + val customFork = "https://github.com/example/wurstStdlib2:custom" + val config = newProjectConfig( + projectName = "custom-stdlib", + dependencies = listOf(customFork), + wc3Patch = "v2.0" + ) + + val aligned = SetupApp.alignedProjectConfig(config, "v3.0") + + Assert.assertEquals(aligned.dependencies, listOf(customFork)) + } + + @Test + fun testExistingV2TargetPinsStdlibBeforeDependencyInstall() { + val config = newProjectConfig( + projectName = "safe-install", + dependencies = listOf("https://github.com/wurstscript/wurstStdlib2"), + wc3Patch = "v2.0" + ) + + val aligned = SetupApp.alignOfficialStdlibDependency(config, "v2.0") + + Assert.assertEquals(aligned.wc3Patch, "v2.0") + Assert.assertEquals( + aligned.dependencies, + listOf("https://github.com/wurstscript/wurstStdlib2:v2.0") + ) + } +} diff --git a/src/test/kotlin/Wc3ClientDetectorTests.kt b/src/test/kotlin/Wc3ClientDetectorTests.kt index ea1caf8..f420e46 100644 --- a/src/test/kotlin/Wc3ClientDetectorTests.kt +++ b/src/test/kotlin/Wc3ClientDetectorTests.kt @@ -71,4 +71,59 @@ class Wc3ClientDetectorTests { Assert.assertNotNull(warning) Assert.assertTrue(warning!!.contains("project targets Reforged")) } + + @Test + fun testReadsExactReforgedVersionFromBuildInfo() { + val root = Files.createTempDirectory("wc3-versioned-reforged") + val exe = Files.createDirectories(root.resolve("_retail_").resolve("x86_64")).resolve("Warcraft III.exe") + Files.writeString(exe, "") + Files.writeString( + root.resolve(".build.info"), + "Branch!STRING:0|Active!DEC:1|Version!STRING:0|Product!STRING:0\n" + + "eu|1|3.0.0.24268|w3\n" + ) + + val info = Wc3ClientDetector.inspectGameRoot(root)!! + + Assert.assertEquals(info.version, "3.0.0.24268") + Assert.assertEquals(info.patchTarget, "v3.0") + Assert.assertNull(Wc3ClientDetector.mismatchMessage("v3.0", info)) + Assert.assertTrue(Wc3ClientDetector.mismatchMessage("v2.0", info)!!.contains("grill patch align")) + } + + @Test + fun testFindsBuildInfoWhenConfiguredPathIsExecutableDirectory() { + val root = Files.createTempDirectory("wc3-configured-bin") + val executableDirectory = Files.createDirectories(root.resolve("_retail_").resolve("x86_64")) + Files.writeString(executableDirectory.resolve("Warcraft III.exe"), "") + Files.writeString( + root.resolve(".build.info"), + "Active!DEC:1|Version!STRING:0|Product!STRING:0\n" + + "1|3.0.0.24268|w3\n" + ) + + val info = Wc3ClientDetector.inspectGameRoot(executableDirectory)!! + + Assert.assertEquals(info.root, root.toAbsolutePath().normalize()) + Assert.assertEquals(info.patchTarget, "v3.0") + } + + @Test + fun testIgnoresInactiveAndNonWarcraftBuildInfoRows() { + val root = Files.createTempDirectory("wc3-multi-product") + val exe = Files.createDirectories(root.resolve("_retail_").resolve("x86_64")).resolve("Warcraft III.exe") + Files.writeString(exe, "") + Files.writeString( + root.resolve(".build.info"), + "Active!DEC:1|Version!STRING:0|Product!STRING:0\n" + + "1|99.0.0.1|other\n" + + "0|2.0.4.23745|w3\n" + + "1|3.0.0.24268|w3\n" + ) + + val info = Wc3ClientDetector.inspectGameRoot(root)!! + + Assert.assertEquals(info.version, "3.0.0.24268") + Assert.assertEquals(info.patchTarget, "v3.0") + } } From 3c05abf71dbee379f539aae4be1198c776f00b9b Mon Sep 17 00:00:00 2001 From: Frotty Date: Sun, 13 Sep 2026 11:16:55 +0200 Subject: [PATCH 2/7] fix patch alignment edge cases --- src/main/kotlin/config/WurstProjectConfig.kt | 6 +++- src/main/kotlin/file/CoreJassProvider.kt | 9 +++++ src/main/kotlin/file/SetupApp.kt | 23 ++++++++---- src/test/kotlin/PatchAlignmentTests.kt | 37 ++++++++++++++++++++ 4 files changed, 68 insertions(+), 7 deletions(-) diff --git a/src/main/kotlin/config/WurstProjectConfig.kt b/src/main/kotlin/config/WurstProjectConfig.kt index 6cfca27..ed3d6a8 100644 --- a/src/main/kotlin/config/WurstProjectConfig.kt +++ b/src/main/kotlin/config/WurstProjectConfig.kt @@ -22,7 +22,11 @@ import java.nio.file.StandardOpenOption */ object WurstProjectConfig { - private val MAPPER = JsonMapper.builder().enable(JsonReadFeature.ALLOW_TRAILING_COMMA).build() + private val MAPPER = JsonMapper.builder() + .enable(JsonReadFeature.ALLOW_TRAILING_COMMA) + .enable(JsonReadFeature.ALLOW_JAVA_COMMENTS) + .enable(JsonReadFeature.ALLOW_YAML_COMMENTS) + .build() private val schema by lazy { javaClass.classLoader.getResource("wbschema.json") } private val log = KotlinLogging.logger {} diff --git a/src/main/kotlin/file/CoreJassProvider.kt b/src/main/kotlin/file/CoreJassProvider.kt index b457e84..149c67a 100644 --- a/src/main/kotlin/file/CoreJassProvider.kt +++ b/src/main/kotlin/file/CoreJassProvider.kt @@ -224,6 +224,15 @@ object CoreJassProvider { return materializedFiles.map { it.path } } + fun managedFilesNeedRefresh(projectRoot: Path, wc3Patch: String?): Boolean { + val buildFolder = projectRoot.resolve("_build") + val previousPatch = readProvenance(buildFolder) ?: return false + val patch = resolveSupportedPatch(wc3Patch) + return previousPatch != patch || + !isValidCoreJassFile(buildFolder.resolve("common.j")) || + !isValidCoreJassFile(buildFolder.resolve("blizzard.j")) + } + fun fetchJassHistoryVersions(): List { val versionListUrl = "$JASS_HISTORY_RAW/$JASS_HISTORY_REF/$VERSION_LIST_FILE" return try { diff --git a/src/main/kotlin/file/SetupApp.kt b/src/main/kotlin/file/SetupApp.kt index d30185a..5ea8408 100644 --- a/src/main/kotlin/file/SetupApp.kt +++ b/src/main/kotlin/file/SetupApp.kt @@ -810,21 +810,32 @@ object SetupApp { } val alignedConfig = alignedProjectConfig(configData, detectedPatch) - if (alignedConfig == configData) { + val configNeedsAlignment = alignedConfig != configData + val coreJassNeedsRefresh = CoreJassProvider.managedFilesNeedRefresh(setup.projectRoot, detectedPatch) + if (!configNeedsAlignment && !coreJassNeedsRefresh) { pass("Project is already aligned with Warcraft III $detectedPatch.") return } if (action != "align") { - log.info("Alignment available: ${currentPatch ?: "unconfigured"} -> $detectedPatch") + if (configNeedsAlignment) { + log.info("Alignment available: ${currentPatch ?: "unconfigured"} -> $detectedPatch") + } + if (coreJassNeedsRefresh) { + log.info("Managed core JASS needs to be refreshed for $detectedPatch.") + } log.info("Run `grill patch align` to update wurst.build, stdlib, and core JASS.") return } - val buildFile = setup.projectRoot.resolve(CONFIG_FILE_NAME) - Files.copy(buildFile, buildFile.resolveSibling("$CONFIG_FILE_NAME.bak"), StandardCopyOption.REPLACE_EXISTING) ensureCoreJassFiles(setup.projectRoot, detectedPatch) - WurstProjectConfig.handleUpdate(setup.projectRoot, clientInfo.root, alignedConfig) - pass("Aligned project with Warcraft III $detectedPatch. Previous config: $CONFIG_FILE_NAME.bak") + if (configNeedsAlignment) { + val buildFile = setup.projectRoot.resolve(CONFIG_FILE_NAME) + Files.copy(buildFile, buildFile.resolveSibling("$CONFIG_FILE_NAME.bak"), StandardCopyOption.REPLACE_EXISTING) + WurstProjectConfig.handleUpdate(setup.projectRoot, clientInfo.root, alignedConfig) + pass("Aligned project with Warcraft III $detectedPatch. Previous config: $CONFIG_FILE_NAME.bak") + } else { + pass("Refreshed managed core JASS for Warcraft III $detectedPatch.") + } } private fun suggestPatchAlignment(configData: WurstProjectConfigData) { diff --git a/src/test/kotlin/PatchAlignmentTests.kt b/src/test/kotlin/PatchAlignmentTests.kt index ffdfb2a..37b6a9b 100644 --- a/src/test/kotlin/PatchAlignmentTests.kt +++ b/src/test/kotlin/PatchAlignmentTests.kt @@ -1,10 +1,12 @@ import config.newProjectConfig +import config.WurstProjectConfig import file.CoreJassProvider import file.CLICommand import file.SetupApp import file.SetupMain import org.testng.Assert import org.testng.annotations.Test +import java.nio.file.Files import java.nio.file.Paths class PatchAlignmentTests { @@ -97,4 +99,39 @@ class PatchAlignmentTests { listOf("https://github.com/wurstscript/wurstStdlib2:v2.0") ) } + + @Test + fun testManagedCoreJassDetectsStalePatchAndInvalidFiles() { + val projectRoot = Files.createTempDirectory("wurstsetup-core-jass-alignment") + val buildFolder = Files.createDirectories(projectRoot.resolve("_build")) + Files.writeString(buildFolder.resolve("core-jass.provenance"), "wc3Patch: v2.0\n") + Files.writeString(buildFolder.resolve("common.j"), "x".repeat(2048)) + Files.writeString(buildFolder.resolve("blizzard.j"), "x".repeat(2048)) + + Assert.assertTrue(CoreJassProvider.managedFilesNeedRefresh(projectRoot, "v3.0")) + Assert.assertFalse(CoreJassProvider.managedFilesNeedRefresh(projectRoot, "v2.0")) + + Files.writeString(buildFolder.resolve("common.j"), "invalid") + Assert.assertTrue(CoreJassProvider.managedFilesNeedRefresh(projectRoot, "v2.0")) + } + + @Test + fun testConfiguredGamePathSupportsJsonc() { + val projectRoot = Files.createTempDirectory("wurstsetup-jsonc-settings") + val vscodeFolder = Files.createDirectories(projectRoot.resolve(".vscode")) + Files.writeString( + vscodeFolder.resolve("settings.json"), + """ + { + // Warcraft III installation used by the Wurst extension. + "wurst.wc3path": "C:\\Games\\Warcraft III", + } + """.trimIndent() + ) + + Assert.assertEquals( + WurstProjectConfig.configuredGamePath(projectRoot), + Paths.get("C:\\Games\\Warcraft III") + ) + } } From 0f98099e5220483532aac5d54c7a2e2ae38b7143 Mon Sep 17 00:00:00 2001 From: Frotty Date: Sun, 13 Sep 2026 11:28:31 +0200 Subject: [PATCH 3/7] keep patch checks read only --- src/main/kotlin/config/WurstProjectConfig.kt | 8 +++++--- src/main/kotlin/file/SetupApp.kt | 5 ++++- src/main/kotlin/file/YamlHelper.kt | 10 +++++++--- src/test/kotlin/PatchAlignmentTests.kt | 14 ++++++++++++++ 4 files changed, 30 insertions(+), 7 deletions(-) diff --git a/src/main/kotlin/config/WurstProjectConfig.kt b/src/main/kotlin/config/WurstProjectConfig.kt index ed3d6a8..2142a06 100644 --- a/src/main/kotlin/config/WurstProjectConfig.kt +++ b/src/main/kotlin/config/WurstProjectConfig.kt @@ -46,14 +46,16 @@ object WurstProjectConfig { } @Throws(IOException::class) - fun loadProject(buildFile: Path): WurstProjectConfigData? { + fun loadProject(buildFile: Path, persistRecovery: Boolean = true): WurstProjectConfigData? { Log.println("Loading project..") if (Files.exists(buildFile) && buildFile.fileName.toString().equals(CONFIG_FILE_NAME, ignoreCase = true)) { - val config = YamlHelper.loadProjectConfig(buildFile) + val config = YamlHelper.loadProjectConfig(buildFile, persistRecovery) val projectRoot = buildFile.parent if (config.projectName.isBlank()) { val namedConfig = config.withProjectName(projectRoot?.fileName?.toString() ?: "unnamed") - saveProjectConfig(projectRoot, namedConfig) + if (persistRecovery) { + saveProjectConfig(projectRoot, namedConfig) + } Log.print("done\n") return namedConfig } diff --git a/src/main/kotlin/file/SetupApp.kt b/src/main/kotlin/file/SetupApp.kt index 5ea8408..b76f11b 100644 --- a/src/main/kotlin/file/SetupApp.kt +++ b/src/main/kotlin/file/SetupApp.kt @@ -138,7 +138,10 @@ object SetupApp { val configFile = setup.projectRoot.resolve(CONFIG_FILE_NAME) var configData: WurstProjectConfigData? = null if (Files.exists(configFile)) { - configData = WurstProjectConfig.loadProject(configFile)!! + configData = WurstProjectConfig.loadProject( + configFile, + persistRecovery = setup.command != CLICommand.PATCH + )!! } when { diff --git a/src/main/kotlin/file/YamlHelper.kt b/src/main/kotlin/file/YamlHelper.kt index 43b8229..c3afe12 100644 --- a/src/main/kotlin/file/YamlHelper.kt +++ b/src/main/kotlin/file/YamlHelper.kt @@ -39,11 +39,13 @@ object YamlHelper { } - fun loadProjectConfig(path: Path): WurstProjectConfigData { + fun loadProjectConfig(path: Path, persistRecovery: Boolean = true): WurstProjectConfigData { val content = Files.readString(path) if (isEffectivelyEmptyYaml(content)) { val fallback = fallbackConfig(path) - persistRecoveredConfig(path, fallback, backupOriginal = false) + if (persistRecovery) { + persistRecoveredConfig(path, fallback, backupOriginal = false) + } return fallback } @@ -54,7 +56,9 @@ object YamlHelper { } catch (e: Exception) { log.warn("The project's wurst.build file could not be read. Recovering with defaults.", e) val fallback = fallbackConfig(path) - persistRecoveredConfig(path, fallback, backupOriginal = true) + if (persistRecovery) { + persistRecoveredConfig(path, fallback, backupOriginal = true) + } fallback } } diff --git a/src/test/kotlin/PatchAlignmentTests.kt b/src/test/kotlin/PatchAlignmentTests.kt index 37b6a9b..080d323 100644 --- a/src/test/kotlin/PatchAlignmentTests.kt +++ b/src/test/kotlin/PatchAlignmentTests.kt @@ -134,4 +134,18 @@ class PatchAlignmentTests { Paths.get("C:\\Games\\Warcraft III") ) } + + @Test + fun testReadOnlyProjectLoadDoesNotRepairMalformedBuildFile() { + val projectRoot = Files.createTempDirectory("wurstsetup-read-only-patch") + val buildFile = projectRoot.resolve("wurst.build") + val malformedConfig = ":\n - [broken" + Files.writeString(buildFile, malformedConfig) + + val loaded = WurstProjectConfig.loadProject(buildFile, persistRecovery = false) + + Assert.assertNotNull(loaded) + Assert.assertEquals(Files.readString(buildFile), malformedConfig) + Assert.assertFalse(Files.exists(projectRoot.resolve("wurst.build.bak"))) + } } From 81cb480123730e9a63a8d9f9103639c9bbcb64aa Mon Sep 17 00:00:00 2001 From: Frotty Date: Sun, 13 Sep 2026 11:38:16 +0200 Subject: [PATCH 4/7] match selected Warcraft client channel --- src/main/kotlin/file/Wc3ClientDetector.kt | 15 ++++++-- src/test/kotlin/Wc3ClientDetectorTests.kt | 42 +++++++++++++++++++++++ 2 files changed, 54 insertions(+), 3 deletions(-) diff --git a/src/main/kotlin/file/Wc3ClientDetector.kt b/src/main/kotlin/file/Wc3ClientDetector.kt index a6787bc..ba3f7b8 100644 --- a/src/main/kotlin/file/Wc3ClientDetector.kt +++ b/src/main/kotlin/file/Wc3ClientDetector.kt @@ -47,7 +47,7 @@ object Wc3ClientDetector { val normalizedRoot = root.toAbsolutePath().normalize() val executable = findExecutable(normalizedRoot) ?: return null val installRoot = installationRootForExecutable(executable) - val version = readBuildInfoVersion(installRoot) + val version = readBuildInfoVersion(installRoot, productForExecutable(executable)) return ClientInfo( installRoot, executable, @@ -134,7 +134,16 @@ object Wc3ClientDetector { return parent } - private fun readBuildInfoVersion(root: Path): String? { + private fun productForExecutable(executable: Path): String? { + val path = executable.toAbsolutePath().normalize().toString().replace('\\', '/').lowercase(Locale.ROOT) + return when { + path.contains("/_ptr_/") -> "w3t" + path.contains("/_retail_/") -> "w3" + else -> null + } + } + + private fun readBuildInfoVersion(root: Path, selectedProduct: String?): String? { val buildInfo = root.resolve(".build.info") if (!Files.isRegularFile(buildInfo)) { return null @@ -151,7 +160,7 @@ object Wc3ClientDetector { .map { it.split('|') } .firstOrNull { values -> values.size > versionIndex && - (productIndex < 0 || values.getOrNull(productIndex).equals("w3", ignoreCase = true)) && + (productIndex < 0 || values.getOrNull(productIndex).equals(selectedProduct ?: "w3", ignoreCase = true)) && (activeIndex < 0 || values.getOrNull(activeIndex) == "1") } ?.getOrNull(versionIndex) diff --git a/src/test/kotlin/Wc3ClientDetectorTests.kt b/src/test/kotlin/Wc3ClientDetectorTests.kt index f420e46..d820210 100644 --- a/src/test/kotlin/Wc3ClientDetectorTests.kt +++ b/src/test/kotlin/Wc3ClientDetectorTests.kt @@ -126,4 +126,46 @@ class Wc3ClientDetectorTests { Assert.assertEquals(info.version, "3.0.0.24268") Assert.assertEquals(info.patchTarget, "v3.0") } + + @Test + fun testSelectsPtrBuildInfoRowForExplicitPtrPath() { + val root = Files.createTempDirectory("wc3-retail-ptr") + val retailExe = Files.createDirectories(root.resolve("_retail_").resolve("x86_64")).resolve("Warcraft III.exe") + val ptrDirectory = Files.createDirectories(root.resolve("_ptr_").resolve("x86_64")) + Files.writeString(retailExe, "") + Files.writeString(ptrDirectory.resolve("Warcraft III.exe"), "") + Files.writeString( + root.resolve(".build.info"), + "Active!DEC:1|Version!STRING:0|Product!STRING:0\n" + + "1|3.0.0.24268|w3\n" + + "1|2.0.4.23745|w3t\n" + ) + + val info = Wc3ClientDetector.inspectGameRoot(ptrDirectory)!! + + Assert.assertEquals(info.executable, ptrDirectory.resolve("Warcraft III.exe")) + Assert.assertEquals(info.version, "2.0.4.23745") + Assert.assertEquals(info.patchTarget, "v2.0") + } + + @Test + fun testSelectsRetailBuildInfoRowForInstallationRoot() { + val root = Files.createTempDirectory("wc3-retail-ptr-root") + val retailDirectory = Files.createDirectories(root.resolve("_retail_").resolve("x86_64")) + val ptrDirectory = Files.createDirectories(root.resolve("_ptr_").resolve("x86_64")) + Files.writeString(retailDirectory.resolve("Warcraft III.exe"), "") + Files.writeString(ptrDirectory.resolve("Warcraft III.exe"), "") + Files.writeString( + root.resolve(".build.info"), + "Active!DEC:1|Version!STRING:0|Product!STRING:0\n" + + "1|2.0.4.23745|w3t\n" + + "1|3.0.0.24268|w3\n" + ) + + val info = Wc3ClientDetector.inspectGameRoot(root)!! + + Assert.assertEquals(info.executable, retailDirectory.resolve("Warcraft III.exe")) + Assert.assertEquals(info.version, "3.0.0.24268") + Assert.assertEquals(info.patchTarget, "v3.0") + } } From afdeca99bb681983d029de1cfce8dadf3204b423 Mon Sep 17 00:00:00 2001 From: Frotty Date: Sun, 13 Sep 2026 11:47:06 +0200 Subject: [PATCH 5/7] preserve selected Warcraft channel path --- src/main/kotlin/file/SetupApp.kt | 2 +- src/main/kotlin/file/Wc3ClientDetector.kt | 18 ++++++++++++++++++ src/test/kotlin/Wc3ClientDetectorTests.kt | 2 ++ 3 files changed, 21 insertions(+), 1 deletion(-) diff --git a/src/main/kotlin/file/SetupApp.kt b/src/main/kotlin/file/SetupApp.kt index b76f11b..8339dd5 100644 --- a/src/main/kotlin/file/SetupApp.kt +++ b/src/main/kotlin/file/SetupApp.kt @@ -834,7 +834,7 @@ object SetupApp { if (configNeedsAlignment) { val buildFile = setup.projectRoot.resolve(CONFIG_FILE_NAME) Files.copy(buildFile, buildFile.resolveSibling("$CONFIG_FILE_NAME.bak"), StandardCopyOption.REPLACE_EXISTING) - WurstProjectConfig.handleUpdate(setup.projectRoot, clientInfo.root, alignedConfig) + WurstProjectConfig.handleUpdate(setup.projectRoot, clientInfo.configuredPath, alignedConfig) pass("Aligned project with Warcraft III $detectedPatch. Previous config: $CONFIG_FILE_NAME.bak") } else { pass("Refreshed managed core JASS for Warcraft III $detectedPatch.") diff --git a/src/main/kotlin/file/Wc3ClientDetector.kt b/src/main/kotlin/file/Wc3ClientDetector.kt index ba3f7b8..ebe1c26 100644 --- a/src/main/kotlin/file/Wc3ClientDetector.kt +++ b/src/main/kotlin/file/Wc3ClientDetector.kt @@ -15,6 +15,7 @@ object Wc3ClientDetector { data class ClientInfo( val root: Path, + val configuredPath: Path, val executable: Path, val kind: ClientKind?, val version: String?, @@ -50,6 +51,7 @@ object Wc3ClientDetector { val version = readBuildInfoVersion(installRoot, productForExecutable(executable)) return ClientInfo( installRoot, + configuredPathForExecutable(executable, installRoot), executable, classifyExecutable(executable), version, @@ -134,6 +136,22 @@ object Wc3ClientDetector { return parent } + private fun configuredPathForExecutable(executable: Path, installRoot: Path): Path { + val executableDirectory = executable.toAbsolutePath().normalize().parent ?: return installRoot + val channelDirectory = if ( + executableDirectory.fileName?.toString()?.equals("x86", ignoreCase = true) == true || + executableDirectory.fileName?.toString()?.equals("x86_64", ignoreCase = true) == true + ) { + executableDirectory.parent + } else { + executableDirectory + } + return channelDirectory?.takeIf { + it.fileName?.toString()?.equals("_retail_", ignoreCase = true) == true || + it.fileName?.toString()?.equals("_ptr_", ignoreCase = true) == true + } ?: installRoot + } + private fun productForExecutable(executable: Path): String? { val path = executable.toAbsolutePath().normalize().toString().replace('\\', '/').lowercase(Locale.ROOT) return when { diff --git a/src/test/kotlin/Wc3ClientDetectorTests.kt b/src/test/kotlin/Wc3ClientDetectorTests.kt index d820210..764c990 100644 --- a/src/test/kotlin/Wc3ClientDetectorTests.kt +++ b/src/test/kotlin/Wc3ClientDetectorTests.kt @@ -144,6 +144,7 @@ class Wc3ClientDetectorTests { val info = Wc3ClientDetector.inspectGameRoot(ptrDirectory)!! Assert.assertEquals(info.executable, ptrDirectory.resolve("Warcraft III.exe")) + Assert.assertEquals(info.configuredPath, ptrDirectory.parent) Assert.assertEquals(info.version, "2.0.4.23745") Assert.assertEquals(info.patchTarget, "v2.0") } @@ -165,6 +166,7 @@ class Wc3ClientDetectorTests { val info = Wc3ClientDetector.inspectGameRoot(root)!! Assert.assertEquals(info.executable, retailDirectory.resolve("Warcraft III.exe")) + Assert.assertEquals(info.configuredPath, retailDirectory.parent) Assert.assertEquals(info.version, "3.0.0.24268") Assert.assertEquals(info.patchTarget, "v3.0") } From 3574699a71b35cf9df7b6c61b77ce09385031566 Mon Sep 17 00:00:00 2001 From: Frotty Date: Sun, 13 Sep 2026 11:57:17 +0200 Subject: [PATCH 6/7] separate client and installation roots --- src/main/kotlin/file/SetupApp.kt | 5 +++-- src/main/kotlin/file/Wc3ClientDetector.kt | 6 +++--- src/test/kotlin/PatchAlignmentTests.kt | 19 +++++++++++++++++++ src/test/kotlin/Wc3ClientDetectorTests.kt | 13 +++++++++---- 4 files changed, 34 insertions(+), 9 deletions(-) diff --git a/src/main/kotlin/file/SetupApp.kt b/src/main/kotlin/file/SetupApp.kt index 8339dd5..5010a1f 100644 --- a/src/main/kotlin/file/SetupApp.kt +++ b/src/main/kotlin/file/SetupApp.kt @@ -782,7 +782,8 @@ object SetupApp { return false } val suffix = dependency.substring(prefix.length) - return suffix.isBlank() || suffix.startsWith(":") + val normalizedSuffix = if (suffix.startsWith(".git", ignoreCase = true)) suffix.substring(4) else suffix + return normalizedSuffix.isBlank() || normalizedSuffix.startsWith(":") } private fun handlePatchAlignment(configData: WurstProjectConfigData) { @@ -834,7 +835,7 @@ object SetupApp { if (configNeedsAlignment) { val buildFile = setup.projectRoot.resolve(CONFIG_FILE_NAME) Files.copy(buildFile, buildFile.resolveSibling("$CONFIG_FILE_NAME.bak"), StandardCopyOption.REPLACE_EXISTING) - WurstProjectConfig.handleUpdate(setup.projectRoot, clientInfo.configuredPath, alignedConfig) + WurstProjectConfig.handleUpdate(setup.projectRoot, clientInfo.root, alignedConfig) pass("Aligned project with Warcraft III $detectedPatch. Previous config: $CONFIG_FILE_NAME.bak") } else { pass("Refreshed managed core JASS for Warcraft III $detectedPatch.") diff --git a/src/main/kotlin/file/Wc3ClientDetector.kt b/src/main/kotlin/file/Wc3ClientDetector.kt index ebe1c26..4bf17fe 100644 --- a/src/main/kotlin/file/Wc3ClientDetector.kt +++ b/src/main/kotlin/file/Wc3ClientDetector.kt @@ -15,7 +15,7 @@ object Wc3ClientDetector { data class ClientInfo( val root: Path, - val configuredPath: Path, + val installationRoot: Path, val executable: Path, val kind: ClientKind?, val version: String?, @@ -50,8 +50,8 @@ object Wc3ClientDetector { val installRoot = installationRootForExecutable(executable) val version = readBuildInfoVersion(installRoot, productForExecutable(executable)) return ClientInfo( + clientRootForExecutable(executable, installRoot), installRoot, - configuredPathForExecutable(executable, installRoot), executable, classifyExecutable(executable), version, @@ -136,7 +136,7 @@ object Wc3ClientDetector { return parent } - private fun configuredPathForExecutable(executable: Path, installRoot: Path): Path { + private fun clientRootForExecutable(executable: Path, installRoot: Path): Path { val executableDirectory = executable.toAbsolutePath().normalize().parent ?: return installRoot val channelDirectory = if ( executableDirectory.fileName?.toString()?.equals("x86", ignoreCase = true) == true || diff --git a/src/test/kotlin/PatchAlignmentTests.kt b/src/test/kotlin/PatchAlignmentTests.kt index 080d323..e177c0f 100644 --- a/src/test/kotlin/PatchAlignmentTests.kt +++ b/src/test/kotlin/PatchAlignmentTests.kt @@ -83,6 +83,25 @@ class PatchAlignmentTests { Assert.assertEquals(aligned.dependencies, listOf(customFork)) } + @Test + fun testAlignmentRecognizesOfficialStdlibGitUrls() { + val config = newProjectConfig( + projectName = "git-url", + dependencies = listOf( + "https://github.com/wurstscript/wurstStdlib2.git", + "https://github.com/wurstscript/wurstStdlib2.git:master" + ), + wc3Patch = "v3.0" + ) + + val aligned = SetupApp.alignedProjectConfig(config, "v2.0") + + Assert.assertEquals( + aligned.dependencies, + listOf("https://github.com/wurstscript/wurstStdlib2:v2.0") + ) + } + @Test fun testExistingV2TargetPinsStdlibBeforeDependencyInstall() { val config = newProjectConfig( diff --git a/src/test/kotlin/Wc3ClientDetectorTests.kt b/src/test/kotlin/Wc3ClientDetectorTests.kt index 764c990..81b0e34 100644 --- a/src/test/kotlin/Wc3ClientDetectorTests.kt +++ b/src/test/kotlin/Wc3ClientDetectorTests.kt @@ -13,7 +13,8 @@ class Wc3ClientDetectorTests { val info = Wc3ClientDetector.inspectGameRoot(root)!! Assert.assertEquals(info.kind, Wc3ClientDetector.ClientKind.REFORGED) - Assert.assertEquals(info.root, root.toAbsolutePath().normalize()) + Assert.assertEquals(info.root, root.resolve("_retail_").toAbsolutePath().normalize()) + Assert.assertEquals(info.installationRoot, root.toAbsolutePath().normalize()) } @Test @@ -58,6 +59,7 @@ class Wc3ClientDetectorTests { Assert.assertEquals(info.kind, Wc3ClientDetector.ClientKind.PRE_129) Assert.assertEquals(info.root, root.toAbsolutePath().normalize()) + Assert.assertEquals(info.installationRoot, root.toAbsolutePath().normalize()) } @Test @@ -104,7 +106,8 @@ class Wc3ClientDetectorTests { val info = Wc3ClientDetector.inspectGameRoot(executableDirectory)!! - Assert.assertEquals(info.root, root.toAbsolutePath().normalize()) + Assert.assertEquals(info.root, root.resolve("_retail_").toAbsolutePath().normalize()) + Assert.assertEquals(info.installationRoot, root.toAbsolutePath().normalize()) Assert.assertEquals(info.patchTarget, "v3.0") } @@ -144,7 +147,8 @@ class Wc3ClientDetectorTests { val info = Wc3ClientDetector.inspectGameRoot(ptrDirectory)!! Assert.assertEquals(info.executable, ptrDirectory.resolve("Warcraft III.exe")) - Assert.assertEquals(info.configuredPath, ptrDirectory.parent) + Assert.assertEquals(info.root, ptrDirectory.parent) + Assert.assertEquals(info.installationRoot, root.toAbsolutePath().normalize()) Assert.assertEquals(info.version, "2.0.4.23745") Assert.assertEquals(info.patchTarget, "v2.0") } @@ -166,7 +170,8 @@ class Wc3ClientDetectorTests { val info = Wc3ClientDetector.inspectGameRoot(root)!! Assert.assertEquals(info.executable, retailDirectory.resolve("Warcraft III.exe")) - Assert.assertEquals(info.configuredPath, retailDirectory.parent) + Assert.assertEquals(info.root, retailDirectory.parent) + Assert.assertEquals(info.installationRoot, root.toAbsolutePath().normalize()) Assert.assertEquals(info.version, "3.0.0.24268") Assert.assertEquals(info.patchTarget, "v3.0") } From 3b99df227be86c4dd22960fd71abfa9894255416 Mon Sep 17 00:00:00 2001 From: Frotty Date: Sun, 13 Sep 2026 12:25:16 +0200 Subject: [PATCH 7/7] harden patch target detection --- src/main/kotlin/file/CoreJassProvider.kt | 7 +++++ src/main/kotlin/file/SetupApp.kt | 2 +- src/main/kotlin/file/Wc3ClientDetector.kt | 6 ++-- src/test/kotlin/GenerateTests.kt | 8 ++++++ src/test/kotlin/Wc3ClientDetectorTests.kt | 34 +++++++++++++++++++++++ 5 files changed, 53 insertions(+), 4 deletions(-) diff --git a/src/main/kotlin/file/CoreJassProvider.kt b/src/main/kotlin/file/CoreJassProvider.kt index 149c67a..4a72bed 100644 --- a/src/main/kotlin/file/CoreJassProvider.kt +++ b/src/main/kotlin/file/CoreJassProvider.kt @@ -194,6 +194,13 @@ object CoreJassProvider { .orElse(false) } + fun isV3OrLaterPatch(input: String?): Boolean { + val patch = normalizePatchInput(input) + val target = Wc3PatchTarget.parse(patch).orElse(null) ?: return false + return target.kind() == Wc3PatchTarget.Kind.REFORGED && + compareVersionStrings(target.gameVersion(), "3.0") >= 0 + } + fun ensureFiles(projectRoot: Path, wc3Patch: String?): List { val buildFolder = projectRoot.resolve("_build") Files.createDirectories(buildFolder) diff --git a/src/main/kotlin/file/SetupApp.kt b/src/main/kotlin/file/SetupApp.kt index 5010a1f..2ffc434 100644 --- a/src/main/kotlin/file/SetupApp.kt +++ b/src/main/kotlin/file/SetupApp.kt @@ -759,7 +759,7 @@ object SetupApp { return when { CoreJassProvider.isPre124(wc3Patch) -> "https://github.com/wurstscript/wurstStdlib2:pre1.24" CoreJassProvider.isPre129Patch(wc3Patch) -> "https://github.com/wurstscript/wurstStdlib2:pre1.29" - CoreJassProvider.patchLine(wc3Patch) == "v3.0" -> "https://github.com/wurstscript/wurstStdlib2" + CoreJassProvider.isV3OrLaterPatch(wc3Patch) -> "https://github.com/wurstscript/wurstStdlib2" else -> "https://github.com/wurstscript/wurstStdlib2:v2.0" } } diff --git a/src/main/kotlin/file/Wc3ClientDetector.kt b/src/main/kotlin/file/Wc3ClientDetector.kt index 4bf17fe..292f795 100644 --- a/src/main/kotlin/file/Wc3ClientDetector.kt +++ b/src/main/kotlin/file/Wc3ClientDetector.kt @@ -172,14 +172,14 @@ object Wc3ClientDetector { val versionIndex = headers.indexOf("Version") val productIndex = headers.indexOf("Product") val activeIndex = headers.indexOf("Active") - if (versionIndex < 0) return null + if (versionIndex < 0 || productIndex < 0 || activeIndex < 0) return null lines.drop(1) .map { it.split('|') } .firstOrNull { values -> values.size > versionIndex && - (productIndex < 0 || values.getOrNull(productIndex).equals(selectedProduct ?: "w3", ignoreCase = true)) && - (activeIndex < 0 || values.getOrNull(activeIndex) == "1") + values.getOrNull(productIndex).equals(selectedProduct ?: "w3", ignoreCase = true) && + values.getOrNull(activeIndex) == "1" } ?.getOrNull(versionIndex) ?.trim() diff --git a/src/test/kotlin/GenerateTests.kt b/src/test/kotlin/GenerateTests.kt index 5f98907..f5c53cc 100644 --- a/src/test/kotlin/GenerateTests.kt +++ b/src/test/kotlin/GenerateTests.kt @@ -166,6 +166,14 @@ class GenerateTests { Assert.assertEquals(SetupApp.stdlibDependencyForPatch("TFT-v1.27b-ru"), legacyStdlib) Assert.assertEquals(SetupApp.stdlibDependencyForPatch("pre1.29"), legacyStdlib) Assert.assertEquals(SetupApp.stdlibDependencyForPatch("v1.29"), preV3Stdlib) + Assert.assertEquals( + SetupApp.stdlibDependencyForPatch("Reforged-v3.1.0.25000-w3-deadbeef"), + currentStdlib + ) + Assert.assertEquals( + SetupApp.stdlibDependencyForPatch("Reforged-v2.0.4.23745-w3-deadbeef"), + preV3Stdlib + ) } @Test(priority = 10) diff --git a/src/test/kotlin/Wc3ClientDetectorTests.kt b/src/test/kotlin/Wc3ClientDetectorTests.kt index 81b0e34..0a489eb 100644 --- a/src/test/kotlin/Wc3ClientDetectorTests.kt +++ b/src/test/kotlin/Wc3ClientDetectorTests.kt @@ -130,6 +130,40 @@ class Wc3ClientDetectorTests { Assert.assertEquals(info.patchTarget, "v3.0") } + @Test + fun testRejectsBuildInfoWithoutProductHeader() { + val root = Files.createTempDirectory("wc3-build-info-no-product") + val exe = Files.createDirectories(root.resolve("_retail_").resolve("x86_64")).resolve("Warcraft III.exe") + Files.writeString(exe, "") + Files.writeString( + root.resolve(".build.info"), + "Active!DEC:1|Version!STRING:0\n" + + "1|3.0.0.24268\n" + ) + + val info = Wc3ClientDetector.inspectGameRoot(root)!! + + Assert.assertNull(info.version) + Assert.assertNull(info.patchTarget) + } + + @Test + fun testRejectsBuildInfoWithoutActiveHeader() { + val root = Files.createTempDirectory("wc3-build-info-no-active") + val exe = Files.createDirectories(root.resolve("_retail_").resolve("x86_64")).resolve("Warcraft III.exe") + Files.writeString(exe, "") + Files.writeString( + root.resolve(".build.info"), + "Version!STRING:0|Product!STRING:0\n" + + "3.0.0.24268|w3\n" + ) + + val info = Wc3ClientDetector.inspectGameRoot(root)!! + + Assert.assertNull(info.version) + Assert.assertNull(info.patchTarget) + } + @Test fun testSelectsPtrBuildInfoRowForExplicitPtrPath() { val root = Files.createTempDirectory("wc3-retail-ptr")