Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
5 changes: 5 additions & 0 deletions src/main/kotlin/file/CLICommand.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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<String>) {
setupMain.devBuild = true
}
},
WITH_AGENTS("--with-agents") {
override fun runOption(setupMain: SetupMain, args: List<String>) {
setupMain.addAgents = true
Expand Down
51 changes: 49 additions & 2 deletions src/main/kotlin/file/SetupApp.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -31,6 +32,11 @@ object SetupApp {

private data class WurstProcessResult(val exitCode: Int, val output: List<String>)

internal const val AGENTS_TEMPLATE_VERSION = "2026-06-10"
private const val AGENTS_TEMPLATE_MARKER_PREFIX = "<!-- WURST_AGENTS_TEMPLATE_VERSION:"
private const val AGENTS_TEMPLATE_MARKER = "<!-- WURST_AGENTS_TEMPLATE_VERSION: $AGENTS_TEMPLATE_VERSION -->"
private const val AGENTS_TEMPLATE_SOURCE_HINT = "WurstScript Warcraft III map project notes"

fun handleArgs(setup: SetupMain) {
this.setup = setup
DependencyManager.debug = setup.debug
Expand Down Expand Up @@ -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 <patch> WC3 patch target: reforged, pre1.29, or jass-history version
Expand Down Expand Up @@ -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")
Expand All @@ -821,6 +863,10 @@ object SetupApp {

args.add("-build")

if (setup.devBuild) {
args.add("-dev")
}
Comment thread
Frotty marked this conversation as resolved.

if (setup.measure) {
args.add("-measure")
}
Expand Down Expand Up @@ -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)?((/)?)")
Expand Down
2 changes: 2 additions & 0 deletions src/main/kotlin/file/SetupMain.kt
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ class SetupMain {

var measure = false

var devBuild = false

var projectRoot: Path = SetupApp.DEFAULT_DIR

var gamePath: Path? = null
Expand Down
23 changes: 23 additions & 0 deletions src/test/kotlin/GenerateTests.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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("<!-- WURST_AGENTS_TEMPLATE_VERSION: ${SetupApp.AGENTS_TEMPLATE_VERSION} -->"))
Assert.assertNull(SetupApp.agentsTemplateWarning(marked))

val oldMarked = "<!-- WURST_AGENTS_TEMPLATE_VERSION: 2026-01-01 -->\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() {
Expand All @@ -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()
Expand Down
82 changes: 80 additions & 2 deletions templates/AGENTS.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
<!-- WURST_AGENTS_TEMPLATE_VERSION: 2026-06-10 -->
# AGENTS.md - WurstScript Map Project Notes

WurstScript Warcraft III map project notes for editing `.wurst` code, dependencies, generated objects, tests, or map build logic.
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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))
Expand Down
Loading