From 264dde14cd1c64bbb72e6bc5a55b1e3932c31261 Mon Sep 17 00:00:00 2001 From: Frotty Date: Thu, 11 Jun 2026 17:53:21 +0200 Subject: [PATCH] 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))