From 264dde14cd1c64bbb72e6bc5a55b1e3932c31261 Mon Sep 17 00:00:00 2001 From: Frotty Date: Thu, 11 Jun 2026 17:53:21 +0200 Subject: [PATCH 1/5] version agents and allow non prod build --- README.md | 7 +++ src/main/kotlin/file/CLICommand.kt | 5 ++ src/main/kotlin/file/SetupApp.kt | 51 ++++++++++++++++++- src/main/kotlin/file/SetupMain.kt | 2 + src/test/kotlin/GenerateTests.kt | 23 +++++++++ templates/AGENTS.md | 82 +++++++++++++++++++++++++++++- 6 files changed, 166 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 8149eec..343d051 100644 --- a/README.md +++ b/README.md @@ -79,6 +79,13 @@ Use `build` to generate an output map according to `wurst.build` specifications. > grill build ``` +Use `--dev` to build the output map in run/development mode. This makes compiletime +`isProductionBuild()` return `false` while still writing a map file. + +```cmd +> grill build ExampleMap.w3x --dev +``` + ## How it works ### Wurst Installation diff --git a/src/main/kotlin/file/CLICommand.kt b/src/main/kotlin/file/CLICommand.kt index 317a831..0f89b82 100644 --- a/src/main/kotlin/file/CLICommand.kt +++ b/src/main/kotlin/file/CLICommand.kt @@ -36,6 +36,11 @@ enum class GlobalOptions(val optionName: String = "", val argCount: Int = 0) { setupMain.measure = true } }, + DEV_BUILD("--dev") { + override fun runOption(setupMain: SetupMain, args: List) { + setupMain.devBuild = true + } + }, WITH_AGENTS("--with-agents") { override fun runOption(setupMain: SetupMain, args: List) { setupMain.addAgents = true diff --git a/src/main/kotlin/file/SetupApp.kt b/src/main/kotlin/file/SetupApp.kt index d6293ae..d99cae8 100644 --- a/src/main/kotlin/file/SetupApp.kt +++ b/src/main/kotlin/file/SetupApp.kt @@ -17,6 +17,7 @@ import org.slf4j.LoggerFactory import org.eclipse.jgit.api.Git import java.awt.GraphicsEnvironment import java.net.URL +import java.nio.charset.StandardCharsets import java.nio.file.Files import java.nio.file.Path import java.nio.file.Paths @@ -31,6 +32,11 @@ object SetupApp { private data class WurstProcessResult(val exitCode: Int, val output: List) + internal const val AGENTS_TEMPLATE_VERSION = "2026-06-10" + private const val AGENTS_TEMPLATE_MARKER_PREFIX = "" + private const val AGENTS_TEMPLATE_SOURCE_HINT = "WurstScript Warcraft III map project notes" + fun handleArgs(setup: SetupMain) { this.setup = setup DependencyManager.debug = setup.debug @@ -144,6 +150,9 @@ object SetupApp { | --quiet Suppress wurst output; only print errors and final result | --debug Print full stack traces for troubleshooting | + |Build options: + | --dev Build with compiletime isProductionBuild() = false + | |Generate options: | --script-mode lua|jass Script mode (default: lua) | --wc3-patch WC3 patch target: reforged, pre1.29, or jass-history version @@ -796,13 +805,46 @@ object SetupApp { private fun downloadAgentsMd(projectDir: Path) { try { - val content = URL("https://raw.githubusercontent.com/wurstscript/WurstSetup/master/templates/AGENTS.md").readText() - Files.write(projectDir.resolve("AGENTS.md"), content.toByteArray()) + val content = withAgentsTemplateMarker(URL("https://raw.githubusercontent.com/wurstscript/WurstSetup/master/templates/AGENTS.md").readText()) + Files.writeString(projectDir.resolve("AGENTS.md"), content, StandardCharsets.UTF_8) log.info("✔ AGENTS.md written.") } catch (e: Exception) { log.warn("⚠️ Could not download AGENTS.md: ${e.message}. Continuing without it.") } } + internal fun withAgentsTemplateMarker(content: String): String { + return if (content.contains(AGENTS_TEMPLATE_MARKER_PREFIX)) { + content + } else { + "$AGENTS_TEMPLATE_MARKER\n$content" + } + } + + internal fun agentsTemplateWarning(content: String): String? { + val markerLine = content.lineSequence().firstOrNull { it.startsWith(AGENTS_TEMPLATE_MARKER_PREFIX) } + if (markerLine == AGENTS_TEMPLATE_MARKER) { + return null + } + if (markerLine != null) { + return "AGENTS.md was generated from an older WurstSetup template ($markerLine). Consider refreshing it from templates/AGENTS.md and re-applying project-local notes." + } + if (content.contains(AGENTS_TEMPLATE_SOURCE_HINT)) { + return "AGENTS.md looks like an older WurstSetup template without a version marker. Consider refreshing it from templates/AGENTS.md and re-applying project-local notes." + } + return null + } + + private fun warnIfAgentsTemplateStale(projectDir: Path) { + val agents = projectDir.resolve("AGENTS.md") + if (!Files.exists(agents)) { + return + } + try { + agentsTemplateWarning(Files.readString(agents, StandardCharsets.UTF_8))?.let { log.warn("⚠️ $it") } + } catch (e: Exception) { + log.warn("⚠️ Could not inspect AGENTS.md template marker: ${e.message}") + } + } fun writeCiWorkflow(projectDir: Path) { val workflowDir = projectDir.resolve(".github/workflows") @@ -821,6 +863,10 @@ object SetupApp { args.add("-build") + if (setup.devBuild) { + args.add("-dev") + } + if (setup.measure) { args.add("-measure") } @@ -1002,6 +1048,7 @@ object SetupApp { private fun handleUpdateProject(configData: WurstProjectConfigData) { WurstProjectConfig.handleUpdate(setup.projectRoot, null, configData) ensureCoreJassFiles(setup.projectRoot, configData.wc3Patch) + warnIfAgentsTemplateStale(setup.projectRoot) } 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 017d61c..0b3e45a 100644 --- a/src/main/kotlin/file/SetupMain.kt +++ b/src/main/kotlin/file/SetupMain.kt @@ -15,6 +15,8 @@ class SetupMain { var measure = false + var devBuild = false + var projectRoot: Path = SetupApp.DEFAULT_DIR var gamePath: Path? = null diff --git a/src/test/kotlin/GenerateTests.kt b/src/test/kotlin/GenerateTests.kt index 7804fcf..d900a77 100644 --- a/src/test/kotlin/GenerateTests.kt +++ b/src/test/kotlin/GenerateTests.kt @@ -249,6 +249,20 @@ class GenerateTests { setup2.parseArgs(listOf("generate", "myproject", "--no-agents")) Assert.assertFalse(setup2.addAgents) } + @Test(priority = 10) + fun testAgentsTemplateMarkerAndWarnings() { + val marked = SetupApp.withAgentsTemplateMarker("# AGENTS.md\n") + Assert.assertTrue(marked.startsWith("")) + Assert.assertNull(SetupApp.agentsTemplateWarning(marked)) + + val oldMarked = "\n# AGENTS.md\n" + Assert.assertTrue(SetupApp.agentsTemplateWarning(oldMarked)!!.contains("older WurstSetup template")) + + val unmarkedGenerated = "# AGENTS.md - WurstScript Map Project Notes\n\nWurstScript Warcraft III map project notes" + Assert.assertTrue(SetupApp.agentsTemplateWarning(unmarkedGenerated)!!.contains("without a version marker")) + + Assert.assertNull(SetupApp.agentsTemplateWarning("# Custom project notes\n")) + } @Test(priority = 10) fun testDebugFlag() { @@ -264,6 +278,15 @@ class GenerateTests { Assert.assertTrue(setup.quiet) } + @Test(priority = 10) + fun testDevBuildFlag() { + val setup = SetupMain() + setup.parseArgs(listOf("build", "ExampleMap.w3x", "--dev")) + Assert.assertEquals(setup.command, CLICommand.BUILD) + Assert.assertEquals(setup.commandArg, "ExampleMap.w3x") + Assert.assertTrue(setup.devBuild) + } + @Test(priority = 10) fun testGenerateWithoutNameUsesWizardPrompt() { val setup = SetupMain() diff --git a/templates/AGENTS.md b/templates/AGENTS.md index 7d67808..eeff5d7 100644 --- a/templates/AGENTS.md +++ b/templates/AGENTS.md @@ -1,3 +1,4 @@ + # AGENTS.md - WurstScript Map Project Notes WurstScript Warcraft III map project notes for editing `.wurst` code, dependencies, generated objects, tests, or map build logic. @@ -137,6 +138,83 @@ Common operators: `+`, `-`, `*`, `/`, `div`, `%`, `mod`, `and`, `or`, `not`, `== let label = count == 1 ? "unit" : "units" ``` +## WurstScript Production Pitfalls + +These are recurring real-world Wurst/Warcraft III failure modes. Treat this section as a pre-edit checklist for any non-trivial Wurst change. + +### Closure capture is by value + +Wurst closures capture locals by value. If a closure assigns to a local from an outer scope, the outer local is not updated. + +Bug pattern: + +```wurst +framehandle clicked = null +dialog.build() -> + clicked = textButton("OK", 0.08, 0.024) +clicked.onClick() -> // clicked is still null outside the build closure + doThing() +``` + +Safer pattern: + +```wurst +dialog.build() -> + let clicked = textButton("OK", 0.08, 0.024) + clicked.onClick() -> + doThing() +``` + +Use `reference(value)` only when a value really must be read or mutated across closure boundaries, and destroy the reference when the owner is done with it: + +```wurst +let clickedRef = reference(null) +dialog.build() -> + clickedRef.val = textButton("OK", 0.08, 0.024) +clickedRef.val.onClick() -> + doThing() +destroy clickedRef +``` + +Prefer avoiding the cross-boundary mutable reference entirely when the handler can be registered inside the closure that creates the frame. + +### Object generation base IDs carry baggage + +Generated object-editor definitions must use real Warcraft III melee objects as base objects, not custom objects generated elsewhere in the map. Custom-object bases can compile into invalid or order-dependent object data. + +Because melee bases carry their own fields, always audit and intentionally clear inherited side effects when creating a generated unit, building, ability, upgrade, or item. Common inherited baggage includes: + +- repair gold/lumber costs and repair time +- melee upgrades used / researches available / tech requirements +- stock, dependency, bounty, collision, food, race, target, and classification fields +- default abilities, autocast/order strings, buffs, art, missile, sound, and tooltip fields + +Prefer local helper presets that explicitly null known-dangerous inherited fields for each object family, then layer the intended fields afterwards. Regression tests for generated object config should assert the absence of known inherited side effects, not only the presence of the new feature. + +### Wurst object lifetime is manual + +Lua output is garbage-collected at the runtime level, but Wurst class lifetimes and destructors are still explicit. Objects created with `new`, closure/listener objects, timers/callbacks, references, collections, layout reports, and many helper wrappers usually need `destroy` when their owner is done. + +Do not rely on "Lua will GC it" if an `ondestroy` cleans up important state, callbacks, frame listeners, arrays, or nested objects. Conversely, do not double-destroy. Wurst instance ids can be reused, so a stale reference may point at a different future object and there is no reliable generic "is this destroyed?" check. Owners must clear stale references themselves after destroy: + +```wurst +if watcher != null + destroy watcher + watcher = null +``` + +### Table UI and layout dependencies + +If a project uses `wurst-table-layout` / `TableUi`, read that dependency's `AGENTS.md`, `AI_USAGE.md`, and `WC3_FRAMEHANDLE_GUIDE.md` before editing UI. Prefer the provided helpers over raw frame code. + +- Load TOC files in `init` when needed, but do not create, move, size, show/hide, reparent, or otherwise manipulate custom frames during blocking map-load init. Delay actual frame work with `doAfter(0.)` or later. +- Build frames under their eventual parent (`withParent(...)` or `dialogFrame(...).build() ->`) rather than creating under a global parent and re-parenting later; WC3 can desync visual and clickable areas after `setParent`. +- Keep root panels, dialogs, dropdowns, and sidecars in the 4:3 safe band with `placeSafe(...)` and declared dimensions. Do not size or place UI from `BlzGetLocalClientWidth()` / `BlzGetLocalClientHeight()` unless guarded against zero/invalid values; minimized clients can report unusable dimensions. +- Avoid on-demand complex frame creation during gameplay when players may be alt-tabbed/minimized. Prefer creating reusable hidden frame trees after map load, then only owner-show/owner-hide/update them. +- Do not move Blizzard default chat/message frames with arbitrary sizes/coords to make room for custom UI. Bad coordinates and default-frame refreshes can crash/desync; create map-owned UI in a safe area instead. +- Register button handlers inside the same build callback that creates the button, or pass the button into a helper immediately. Do not assign a button/frame to an outer local inside a build callback and call `.onClick()` on that outer local afterwards. +- Prefer table-wide defaults for repeated alignment, such as `layout.defaultHalign(Align.CENTER)`, instead of writing `..center()` on every row. Use per-row alignment calls only for exceptions. +- Hide and reuse multiplayer UI frame trees. Do not destroy/recreate framehandles during gameplay cleanup. ## Packages and API Shape - Package members are private by default; use `public` for exports. @@ -182,7 +260,7 @@ doAfter(1.) -> print("later") ``` -Closures capture locals by value. Stored/object-backed closures often need cleanup. Lambdas used as `code` cannot take parameters or capture locals. +Closures capture locals by value. Stored/object-backed closures often need cleanup. Use `reference(...)` for intentional cross-closure mutation, and destroy the reference when finished. Lambdas used as `code` cannot take parameters or capture locals. ## Classes, Tuples, Generics @@ -223,7 +301,7 @@ Old `T` generics erase through integer casts and can share storage. ## Compiletime and Objects -Use compiletime generation for object-editor data. Prefer wrappers and ID generators so IDs stay stable and collision-free. +Use compiletime generation for object-editor data. Prefer wrappers and ID generators so IDs stay stable and collision-free. Generated objects must use melee base objects, then explicitly clear inherited fields that would create unwanted side effects. ```wurst let value = compiletime(fac(5)) From 82e0baf65148a05ca8d9f8fed660d59424c66efd Mon Sep 17 00:00:00 2001 From: Frotty Date: Mon, 22 Jun 2026 12:36:02 +0200 Subject: [PATCH 2/5] make quiet more quiet --- src/main/kotlin/file/SetupApp.kt | 94 ++++++++++++++++++++++++++------ src/test/kotlin/GenerateTests.kt | 54 ++++++++++++++++++ 2 files changed, 132 insertions(+), 16 deletions(-) diff --git a/src/main/kotlin/file/SetupApp.kt b/src/main/kotlin/file/SetupApp.kt index d99cae8..d04f67a 100644 --- a/src/main/kotlin/file/SetupApp.kt +++ b/src/main/kotlin/file/SetupApp.kt @@ -90,12 +90,16 @@ object SetupApp { } private fun fail(message: String) { - log.error(message) + if (setup.quiet) { + System.err.println(message) + } else { + log.error(message) + } } private fun detail(message: String) { if (setup.quiet) { - println(message) + System.err.println(message) } else { log.info(message) } @@ -366,13 +370,15 @@ object SetupApp { if (printPjassFailure(result.output)) { return } - fail("❌ Wurst $commandName failed.") - detail("Exit code: ${result.exitCode}") if (setup.quiet) { - detail("Next: rerun without `--quiet` only for the failed file/test.") - } else { - detail("Try: rerun with `--quiet` for a shorter error log, or `--debug` for troubleshooting details.") + val diagnostics = quietCompilerDiagnostics(result.output) + diagnostics.forEach { System.err.println(it) } + fail("❌ Wurst $commandName failed. (Errors: ${quietCompilerErrorCount(result.output, diagnostics)})") + return } + fail("❌ Wurst $commandName failed.") + detail("Exit code: ${result.exitCode}") + detail("Try: rerun with `--quiet` for a shorter error log, or `--debug` for troubleshooting details.") } private fun printPjassFailure(output: List): Boolean { @@ -402,14 +408,75 @@ object SetupApp { return true } - private fun isImportantCompilerLine(line: String): Boolean { - return line.contains("error", ignoreCase = true) || - line.contains("warning", ignoreCase = true) || - line.contains("FAILED", ignoreCase = true) || + internal fun quietCompilerDiagnostics(output: List): List { + val diagnostics = ArrayList() + var pendingVerboseError: MatchResult? = null + + for (rawLine in output) { + val line = rawLine.trimEnd() + if (line.isBlank() || isNoisyCompilerVersionLine(line) || isQuietCompilerNoiseLine(line)) { + continue + } + + val verboseError = Regex("""^Error in File (.+):(\d+):\s*$""").find(line.trim()) + if (verboseError != null) { + pendingVerboseError = verboseError + continue + } + + if (pendingVerboseError != null) { + diagnostics.add( + "Error ${pendingVerboseError.groupValues[1]}:${pendingVerboseError.groupValues[2]}: ${line.trim()}" + ) + pendingVerboseError = null + continue + } + + if (isQuietCompilerDiagnosticLine(line)) { + diagnostics.add(line) + } + } + + return diagnostics.distinct() + } + + internal fun quietCompilerErrorCount( + output: List, + diagnostics: List = quietCompilerDiagnostics(output) + ): Int { + output.asSequence() + .map { Regex("""^Errors:\s*(\d+)\s*$""").find(it.trim()) } + .filterNotNull() + .firstOrNull() + ?.let { return it.groupValues[1].toIntOrNull() ?: diagnostics.size.coerceAtLeast(1) } + + return diagnostics.count { + it.startsWith("Error ", ignoreCase = true) || + it.startsWith("FAILED ", ignoreCase = true) || + it.contains(" exception", ignoreCase = true) || + it.contains("Pjass", ignoreCase = true) + }.coerceAtLeast(1) + } + + private fun isQuietCompilerDiagnosticLine(line: String): Boolean { + return line.startsWith("Error ", ignoreCase = true) || + line.startsWith("FAILED ", ignoreCase = true) || + line.contains(" assertion", ignoreCase = true) || line.contains("Exception", ignoreCase = true) || line.contains("Pjass", ignoreCase = true) } + private fun isQuietCompilerNoiseLine(line: String): Boolean { + val trimmed = line.trim() + return trimmed.startsWith("Warning", ignoreCase = true) || + trimmed.matches(Regex("""^Errors:\s*\d+\s*$""")) || + trimmed.matches(Regex("""^Warnings:\s*\d+\s*$""")) || + trimmed.matches(Regex("""^Tests:\s*\d+/\d+\s+passed\s*$""", RegexOption.IGNORE_CASE)) || + trimmed.startsWith("compilation finished", ignoreCase = true) || + trimmed.startsWith("Running tests", ignoreCase = true) || + trimmed.startsWith("Finished running tests", ignoreCase = true) + } + private fun isNoisyCompilerVersionLine(line: String): Boolean { val trimmed = line.trim() return trimmed == "Warning: Ignoring unknown wc3Patch in wurst.build: ${CoreJassProvider.DEFAULT_PATCH}" || @@ -960,11 +1027,6 @@ object SetupApp { } } val exitCode = p.waitFor() - if (setup.quiet && exitCode != 0) { - val printableOutput = if (setup.debug) output else output.filterNot(::isNoisyCompilerVersionLine) - val linesToPrint = if (compactFallback) printableOutput.filter(::isImportantCompilerLine) else printableOutput - linesToPrint.forEach { println(it) } - } return WurstProcessResult(exitCode, output) } diff --git a/src/test/kotlin/GenerateTests.kt b/src/test/kotlin/GenerateTests.kt index d900a77..311687e 100644 --- a/src/test/kotlin/GenerateTests.kt +++ b/src/test/kotlin/GenerateTests.kt @@ -278,6 +278,60 @@ class GenerateTests { Assert.assertTrue(setup.quiet) } + @Test(priority = 10) + fun testQuietCompilerDiagnosticsSuppressGeneratedJassNoise() { + val output = listOf( + "Warnings: 3", + "Warning: Error: e:Could not find variable silverGladeCounter.", + "Warning: Error: e:Could not find a function with name eg", + "Error Broken.wurst:12: Could not find variable realUserTypo.", + "compilation finished (errors: 1, warnings: 3)", + "Errors: 1" + ) + + Assert.assertEquals( + SetupApp.quietCompilerDiagnostics(output), + listOf("Error Broken.wurst:12: Could not find variable realUserTypo.") + ) + Assert.assertEquals(SetupApp.quietCompilerErrorCount(output), 1) + } + + @Test(priority = 10) + fun testQuietCompilerDiagnosticsKeepFailedTestDetails() { + val output = listOf( + "Running tests", + "Tests: 1/2 passed", + "FAILED MyPkg.testExplodes", + "Errors: 1", + "Error MyTest.wurst:9: expected 1 but got 2", + "Finished running tests" + ) + + Assert.assertEquals( + SetupApp.quietCompilerDiagnostics(output), + listOf( + "FAILED MyPkg.testExplodes", + "Error MyTest.wurst:9: expected 1 but got 2" + ) + ) + Assert.assertEquals(SetupApp.quietCompilerErrorCount(output), 1) + } + + @Test(priority = 10) + fun testQuietCompilerDiagnosticsNormalizeVerboseFallbackErrors() { + val output = listOf( + "Error in File Broken.wurst:12:", + " Could not find variable realUserTypo.", + "Warning in File war3map.j:44:", + " Error: e:Could not find variable silverGladeCounter." + ) + + Assert.assertEquals( + SetupApp.quietCompilerDiagnostics(output), + listOf("Error Broken.wurst:12: Could not find variable realUserTypo.") + ) + } + @Test(priority = 10) fun testDevBuildFlag() { val setup = SetupMain() From 301c78a69038c055a35074030ff2755282f441b9 Mon Sep 17 00:00:00 2001 From: Frotty Date: Mon, 22 Jun 2026 13:00:30 +0200 Subject: [PATCH 3/5] agents updates --- src/main/kotlin/file/SetupApp.kt | 22 +++++++++++++++- src/test/kotlin/GenerateTests.kt | 15 +++++++++++ templates/AGENTS.md | 44 ++++++++++++++++++++++++++++++-- 3 files changed, 78 insertions(+), 3 deletions(-) diff --git a/src/main/kotlin/file/SetupApp.kt b/src/main/kotlin/file/SetupApp.kt index d04f67a..72af86f 100644 --- a/src/main/kotlin/file/SetupApp.kt +++ b/src/main/kotlin/file/SetupApp.kt @@ -32,7 +32,7 @@ object SetupApp { private data class WurstProcessResult(val exitCode: Int, val output: List) - internal const val AGENTS_TEMPLATE_VERSION = "2026-06-10" + internal const val AGENTS_TEMPLATE_VERSION = "2026-06-22" private const val AGENTS_TEMPLATE_MARKER_PREFIX = "" private const val AGENTS_TEMPLATE_SOURCE_HINT = "WurstScript Warcraft III map project notes" @@ -411,6 +411,7 @@ object SetupApp { internal fun quietCompilerDiagnostics(output: List): List { val diagnostics = ArrayList() var pendingVerboseError: MatchResult? = null + var preservingTestFailureDetails = false for (rawLine in output) { val line = rawLine.trimEnd() @@ -432,8 +433,16 @@ object SetupApp { continue } + if (preservingTestFailureDetails && isQuietTestFailureDetailLine(line)) { + diagnostics.add(line) + continue + } + if (isQuietCompilerDiagnosticLine(line)) { diagnostics.add(line) + if (isQuietTestFailureHeader(line)) { + preservingTestFailureDetails = true + } } } @@ -466,6 +475,17 @@ object SetupApp { line.contains("Pjass", ignoreCase = true) } + private fun isQuietTestFailureHeader(line: String): Boolean { + return line.trim().equals("FAILED assertion:", ignoreCase = true) + } + + private fun isQuietTestFailureDetailLine(line: String): Boolean { + val trimmed = line.trim() + return trimmed.startsWith("Test failed:", ignoreCase = true) || + trimmed.contains(" inside call ", ignoreCase = true) || + trimmed.contains(" when calling ", ignoreCase = true) + } + private fun isQuietCompilerNoiseLine(line: String): Boolean { val trimmed = line.trim() return trimmed.startsWith("Warning", ignoreCase = true) || diff --git a/src/test/kotlin/GenerateTests.kt b/src/test/kotlin/GenerateTests.kt index 311687e..030c8f5 100644 --- a/src/test/kotlin/GenerateTests.kt +++ b/src/test/kotlin/GenerateTests.kt @@ -8,6 +8,7 @@ import file.SetupMain import org.testng.Assert import org.testng.annotations.Test import java.nio.file.Files +import java.nio.file.Paths import java.util.Comparator private class ExitException2(val code: Int) : RuntimeException("exit $code") @@ -251,6 +252,12 @@ class GenerateTests { } @Test(priority = 10) fun testAgentsTemplateMarkerAndWarnings() { + val templateFirstLine = Files.readAllLines(Paths.get("templates", "AGENTS.md")).first() + Assert.assertEquals( + templateFirstLine, + "" + ) + val marked = SetupApp.withAgentsTemplateMarker("# AGENTS.md\n") Assert.assertTrue(marked.startsWith("")) Assert.assertNull(SetupApp.agentsTemplateWarning(marked)) @@ -302,6 +309,10 @@ class GenerateTests { "Running tests", "Tests: 1/2 passed", "FAILED MyPkg.testExplodes", + "\tFAILED assertion:", + "\tTest failed: expected 1 but got 2", + "\t ╚ MyTest.wurst:9 inside call assertEquals(1, 2)", + "\t... when calling MyPkg.testExplodes(MyTest.wurst:12)", "Errors: 1", "Error MyTest.wurst:9: expected 1 but got 2", "Finished running tests" @@ -311,6 +322,10 @@ class GenerateTests { SetupApp.quietCompilerDiagnostics(output), listOf( "FAILED MyPkg.testExplodes", + "\tFAILED assertion:", + "\tTest failed: expected 1 but got 2", + "\t ╚ MyTest.wurst:9 inside call assertEquals(1, 2)", + "\t... when calling MyPkg.testExplodes(MyTest.wurst:12)", "Error MyTest.wurst:9: expected 1 but got 2" ) ) diff --git a/templates/AGENTS.md b/templates/AGENTS.md index eeff5d7..04476f0 100644 --- a/templates/AGENTS.md +++ b/templates/AGENTS.md @@ -1,4 +1,4 @@ - + # AGENTS.md - WurstScript Map Project Notes WurstScript Warcraft III map project notes for editing `.wurst` code, dependencies, generated objects, tests, or map build logic. @@ -8,11 +8,42 @@ WurstScript Warcraft III map project notes for editing `.wurst` code, dependenci - Prefer simple, maintainable code. Fix root causes; avoid brittle workarounds, duplicated branches, and special-case patches. - Keep packages focused and below ~500 lines; split by feature, responsibility, or data type. - Make changes in the source package, not generated output. Do not edit `_build/` or `_build/dependencies/` as source-of-truth. -- Prefer Wurst standard-library wrappers and project helpers over raw `common.j`/Jass-style calls. +- Use Wurst stdlib/library APIs and project helpers; never call a raw `common.j`/Jass native when a wrapper exists, and do not reinvent what stdlib already provides. See **Stdlib-First** below. - When unsure about Wurst syntax or local APIs, inspect nearby working code before guessing. - Keep tests narrow. Add/update tests for behavior, parsing, compiletime generation, or shared utilities. - Avoid broad refactors unless they directly reduce risk or complexity for the requested change. +## Stdlib-First: No Raw JASS Natives (Mandatory) + +The most important coding rule: high-level Wurst packages must use the WurstScript stdlib and library APIs, never ported JASS. The goal is clean, reusable Wurst — not a JASS transliteration. + +- Never call a raw `common.j` / `Blizzard.j` native when a Wurst wrapper or extension function exists. There is one for almost every native (on `unit`, `player`, `group`, `string`, `rect`, ...). Grep the stdlib (`_build/dependencies/wurstStdlib2/wurst/`) before writing a native call. +- The only bar for a raw native is that you searched and confirmed no wrapper exists — then add a one-line comment saying so. +- "It compiles" is not enough. Code that reads like JASS (manual handle juggling, native calls, global trigger callbacks, op-limit chunking) is wrong here; rewrite it idiomatically. + +Use the stdlib API, not a raw native, for at least: + +- Timers → `ClosureTimers` (`doAfter`, `doPeriodically`); never `CreateTimer`/`TimerStart`/`PauseTimer`/`DestroyTimer`. +- Printing → `print` / `printTimed` / `p.print`; never `DisplayText*ToPlayer`/`...ToForce`. +- Player state → `Player` extensions (`p.addGold`, `p.getGold`, `p.getId`, ...); prefer the `players[i]` array over `Player(i)`. +- Unit inspection → `Unit` extensions (`u.getTypeId()`, `u.getOwner()`, `u.getAbilityLevel(id)`, ...). +- Hashtables → `Hashtable` extensions (`ht.saveInt`/`loadInt`/`flushChild`/...). +- Group iteration → `ClosureForGroups` (`forUnitsInRange`, `forUnitsInRect`) + `GroupUtils` (`getGroup()` / `group.release()`), not `GroupEnum*` + `ForGroup` globals. + +The `CreateTrigger()..register...()..addAction() ->` cascade is the accepted idiom and is fine. + +### Do Not Reinvent Stdlib Infrastructure (Mandatory) + +Keep custom engine-level infrastructure to a minimum. Stdlib packages are battle-tested and handle the WC3 edge cases (recycling, op-limits, cleanup, desync) that hand-rolled versions get wrong. Grep for an existing system before building one; do not ship a parallel implementation of something stdlib provides: + +- Dummy spell casting → `DummyCaster` / `InstantDummyCaster` (unit pooling: `DummyRecycler`). +- Triggered damage → `DummyDamage` to deal, `DamageEvent` to detect/modify. +- Events → `ClosureEvents` (`EventListener.add(...)`) / `RegisterEvents`; no custom global-trigger dispatcher or event bus. +- Knockback / FX / sound / interpolation / orders → `Knockback3`, `Fx`, `SoundUtils`/`Sounds`, `Interpolation`, `Orders`/`OrderStringFactory`. +- Collections → `LinkedList`, `HashMap`, `HashList`. + +If stdlib almost fits, prefer a thin wrapper around the stdlib type over a from-scratch system and note why in a comment. Reinventing this is treated as a defect even if it compiles and passes tests, because it reintroduces solved bugs. + ## Agent Workflow Install dependencies: @@ -142,6 +173,15 @@ let label = count == 1 ? "unit" : "units" These are recurring real-world Wurst/Warcraft III failure modes. Treat this section as a pre-edit checklist for any non-trivial Wurst change. +### Integer overflow + +WC3 `int` is 32-bit signed and wraps silently at ~2.1 billion (`2^31 - 1`) — no exception, just a negative/garbage value that poisons every downstream comparison and division. Easy to hit when multiplying or summing large game quantities (gold/worth, army totals, damage products, accumulated stats). + +- Promote to `real` BEFORE multiplying two large quantities: `a.toReal() * b`, never `(a * b).toReal()` (the latter already overflowed). +- Same for running sums of products: `total += worth.toReal() * count * mult`. +- Watch `worth * worth`, `count * worth`, `total * total` in scoring/stats; aggregate worths routinely exceed ~46k (the square root of int-max), so their product overflows in ordinary large games. +- Wurst `/` is real division even for two ints (use `div` for integer division), so division itself does not overflow — but its operands still can. Prefer `real` for any accumulator that fans in many large terms. + ### Closure capture is by value Wurst closures capture locals by value. If a closure assigns to a local from an outer scope, the outer local is not updated. From e7d7e4e12d82ca456ab9de8b3e460236a75b37d5 Mon Sep 17 00:00:00 2001 From: Frotty Date: Mon, 22 Jun 2026 13:14:17 +0200 Subject: [PATCH 4/5] review fixes --- src/main/kotlin/file/SetupApp.kt | 6 +++++- src/test/kotlin/GenerateTests.kt | 19 +++++++++++++++++++ 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/src/main/kotlin/file/SetupApp.kt b/src/main/kotlin/file/SetupApp.kt index 72af86f..7940c46 100644 --- a/src/main/kotlin/file/SetupApp.kt +++ b/src/main/kotlin/file/SetupApp.kt @@ -371,7 +371,7 @@ object SetupApp { return } if (setup.quiet) { - val diagnostics = quietCompilerDiagnostics(result.output) + val diagnostics = quietCompilerFailureOutput(result.output, setup.debug) diagnostics.forEach { System.err.println(it) } fail("❌ Wurst $commandName failed. (Errors: ${quietCompilerErrorCount(result.output, diagnostics)})") return @@ -408,6 +408,10 @@ object SetupApp { return true } + internal fun quietCompilerFailureOutput(output: List, debug: Boolean): List { + return if (debug) output else quietCompilerDiagnostics(output) + } + internal fun quietCompilerDiagnostics(output: List): List { val diagnostics = ArrayList() var pendingVerboseError: MatchResult? = null diff --git a/src/test/kotlin/GenerateTests.kt b/src/test/kotlin/GenerateTests.kt index 030c8f5..ec34844 100644 --- a/src/test/kotlin/GenerateTests.kt +++ b/src/test/kotlin/GenerateTests.kt @@ -347,6 +347,25 @@ class GenerateTests { ) } + @Test(priority = 10) + fun testQuietDebugCompilerFailureOutputBypassesFilter() { + val output = listOf( + "Error Broken.wurst:12: Could not find variable realUserTypo.", + "java.lang.IllegalStateException: extra debug context", + "\tat de.peeeq.wurstio.Main.main(Main.java:1)", + "Warning: Error: e:Could not find variable generatedNoise." + ) + + Assert.assertEquals(SetupApp.quietCompilerFailureOutput(output, debug = true), output) + Assert.assertEquals( + SetupApp.quietCompilerFailureOutput(output, debug = false), + listOf( + "Error Broken.wurst:12: Could not find variable realUserTypo.", + "java.lang.IllegalStateException: extra debug context" + ) + ) + } + @Test(priority = 10) fun testDevBuildFlag() { val setup = SetupMain() From 36c0b4cb28c83e32b45925f80677588ee8939e39 Mon Sep 17 00:00:00 2001 From: Frotty Date: Mon, 22 Jun 2026 14:30:02 +0200 Subject: [PATCH 5/5] Update AGENTS.md --- templates/AGENTS.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/templates/AGENTS.md b/templates/AGENTS.md index 04476f0..d3370ad 100644 --- a/templates/AGENTS.md +++ b/templates/AGENTS.md @@ -74,6 +74,8 @@ For build changes: grill build ExampleMap.w3x --quiet ``` +Builds default to production mode, so compiletime `isProductionBuild()` returns `true`. Use `grill build ExampleMap.w3x --dev --quiet` only when validating run/development-mode behavior where `isProductionBuild()` must be `false`; `typecheck` and `test` do not need this flag. + Done means relevant errors/warnings are fixed or explicitly explained. ## Project Configuration