diff --git a/AGENTS.md b/AGENTS.md index e1b71c6..613988c 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -2,6 +2,13 @@ This repo builds the Grill CLI and project setup tooling. The generated map-project agent notes live in `templates/AGENTS.md`; keep this root file focused on WurstSetup itself. +## Editing templates/AGENTS.md + +- The first line marker must stay in lockstep with `SetupApp.AGENTS_TEMPLATE_VERSION` (a test enforces this); bump both when changing the template. +- Keep the intro line starting with "WurstScript Warcraft III map project notes" — `AGENTS_TEMPLATE_SOURCE_HINT` matches on it to detect template-derived files. +- Grill fetches the template from the raw GitHub URL on master, so pushing template changes publishes them to `grill install` users immediately. Only document language features after a compiler release ships them. +- Keep the template token-lean: it is loaded into every agent session in user projects. Prefer pointers to on-demand docs (stdlib/dependency `AGENTS.md`, the online manual) over inlining more content. + ## Current Architecture - Project config models come from `com.github.wurstscript:wurst-project-config`; do not reintroduce local DAO copies. diff --git a/src/main/kotlin/file/SetupApp.kt b/src/main/kotlin/file/SetupApp.kt index bb92658..4b191c8 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-22" + internal const val AGENTS_TEMPLATE_VERSION = "2026-08-05" private const val AGENTS_TEMPLATE_MARKER_PREFIX = "" private const val AGENTS_TEMPLATE_SOURCE_HINT = "WurstScript Warcraft III map project notes" @@ -107,7 +107,7 @@ object SetupApp { private fun handleCMD() { // Cold-start lever: only spend network round-trips and a compiler subprocess on commands - // that actually consult the installation. help/generate stay fully offline and fast. + // that actually consult the installation. help/generate avoid installation checks and stay fast. when { setup.command == CLICommand.INSTALL && setup.commandArg.equals("wurstscript", ignoreCase = true) -> { // Needs to know whether a newer compiler is available online. @@ -367,6 +367,10 @@ object SetupApp { } private fun resolveGenerateGamePath(setup: SetupMain, wc3Patch: String?): Path? { + if (setup.gamePathOptedOut) { + log.info("Warcraft III path: not configured by choice.") + return null + } val gameRoot = setup.gamePath ?: Wc3ClientDetector.detectGameRoot() val clientInfo = Wc3ClientDetector.inspectGameRoot(gameRoot) if (clientInfo == null) { @@ -546,10 +550,6 @@ object SetupApp { internal var installPatchPrompt: ((String, String?) -> String?)? = null internal fun prepareGenerate(setup: SetupMain): Boolean { - if (setup.commandArg.isNotBlank()) { - return true - } - val prompt = generatePrompt ?: terminalPrompt() while (setup.commandArg.isBlank()) { @@ -561,20 +561,16 @@ object SetupApp { } } - runWizard(setup, prompt, useInteractiveMenus = generatePrompt == null) + runWizard(setup, prompt, useInteractiveMenus = generatePrompt == null && TerminalMenu.canUseInteractive()) return true } private fun terminalPrompt(): (String, String?) -> String? { val console = System.console() if (console == null) { - return prompt@ { message, default -> - if (default == null) { - print("$message: ") - } else { - print("$message [$default]: ") - } - val input = readlnOrNull()?.trim() ?: return@prompt null + // Non-console stdin is script input; keep EOF/default generation quiet. + return prompt@ { _, default -> + val input = readlnOrNull()?.trim() ?: return@prompt default input.ifEmpty { default } } } @@ -599,15 +595,16 @@ object SetupApp { useInteractiveMenus = useInteractiveMenus, currentPatch = setup.wc3Patch ) - setup.gamePath = selectGamePath(prompt, setup.wc3Patch, setup.gamePath) + setup.gamePathOptedOut = false + setup.gamePath = selectGamePath(setup, prompt, setup.wc3Patch, setup.gamePath) val agentsDefault = if (setup.addAgents) "Y" else "N" - val agentsInput = prompt("Add AGENTS.md?", agentsDefault) - setup.addAgents = agentsInput?.lowercase() == "y" + val agentsInput = prompt("Add AGENTS.md?", agentsDefault) ?: agentsDefault + setup.addAgents = agentsInput.lowercase() == "y" val ciDefault = if (setup.addGithubWorkflow) "Y" else "N" - val ciInput = prompt("Add GitHub Actions CI?", ciDefault) - setup.addGithubWorkflow = ciInput?.lowercase() == "y" + val ciInput = prompt("Add GitHub Actions CI?", ciDefault) ?: ciDefault + setup.addGithubWorkflow = ciInput.lowercase() == "y" setup.curatedDependencyIds = selectCuratedDependencies(prompt, setup.curatedDependencyIds).toMutableList() } @@ -625,8 +622,8 @@ object SetupApp { val selected = LinkedHashSet(preselectedIds) for (dependency in catalog) { val default = if (selected.contains(dependency.id)) "Y" else "N" - val answer = prompt("Add ${dependency.summary}?", default) - if (answer?.trim()?.lowercase() == "y") { + val answer = prompt("Add ${dependency.summary}?", default) ?: default + if (answer.trim().lowercase() == "y") { selected.add(dependency.id) } else { selected.remove(dependency.id) @@ -636,6 +633,7 @@ object SetupApp { } private fun selectGamePath( + setup: SetupMain, prompt: (String, String?) -> String?, wc3Patch: String?, currentPath: Path? @@ -652,12 +650,14 @@ object SetupApp { val default = detected?.toAbsolutePath()?.normalize()?.toString() ?: "none" val answer = prompt("Warcraft III directory (or none)", default)?.trim() ?: return detected if (answer.equals("none", ignoreCase = true) || answer.equals("skip", ignoreCase = true)) { + setup.gamePathOptedOut = true return null } val selected = Paths.get(answer).toAbsolutePath().normalize() val selectedInfo = Wc3ClientDetector.inspectGameRoot(selected) if (selectedInfo == null) { log.warn("No supported Warcraft III executable found in $selected. You can fix wurst.wc3path later in .vscode/settings.json.") + setup.gamePathOptedOut = true return null } Wc3ClientDetector.mismatchMessage(wc3Patch, selectedInfo)?.let { log.warn(it) } @@ -760,37 +760,38 @@ object SetupApp { useInteractiveMenus: Boolean, currentPatch: String? ): String { - val versions = CoreJassProvider.fetchJassHistoryVersions() - val recommended = CoreJassProvider.recommendedPatchOptions(versions) + val bundledVersions = CoreJassProvider.supportedPatches + val recommended = CoreJassProvider.recommendedPatchOptions(bundledVersions) val patchTargets = CoreJassProvider.supportedPatches + val exactVersions by lazy { CoreJassProvider.fetchJassHistoryVersions() } val normalizedCurrentPatch = currentPatch?.let(CoreJassProvider::normalizePatchInput) val defaultPatch = when { normalizedCurrentPatch != null && CoreJassProvider.isSupportedPatch(normalizedCurrentPatch) -> normalizedCurrentPatch else -> recommended.firstOrNull() ?: CoreJassProvider.DEFAULT_PATCH } val browseAll = "__browse_all__" + val visibleRecommended = (listOf(defaultPatch) + recommended).distinct() if (useInteractiveMenus) { while (true) { - val choices = recommended.map { TerminalMenu.Choice(it, CoreJassProvider.describePatch(it)) } + + val choices = visibleRecommended.map { TerminalMenu.Choice(it, CoreJassProvider.describePatch(it)) } + TerminalMenu.Choice(browseAll, "Browse all supported patch targets...") + TerminalMenu.Choice("__browse_exact__", "Advanced: browse exact jass-history dumps...") val selection = TerminalMenu.choose( title = intro, choices = choices, - defaultIndex = recommended.indexOf(defaultPatch).takeIf { it >= 0 } ?: 0 + defaultIndex = visibleRecommended.indexOf(defaultPatch).takeIf { it >= 0 } ?: 0 ) when { selection == null -> return defaultPatch selection == browseAll -> browsePatchVersionsInteractive("WC3 patch targets", patchTargets)?.let { return it } - selection == "__browse_exact__" -> browsePatchVersionsInteractive("Exact jass-history dumps", versions)?.let { return it } + selection == "__browse_exact__" -> browsePatchVersionsInteractive("Exact jass-history dumps", exactVersions)?.let { return it } else -> return selection } } } log.info(intro) - val visibleRecommended = (listOf(defaultPatch) + recommended).distinct() log.info("Recommended patch choices:") visibleRecommended.forEachIndexed { index, patch -> log.info(" ${index + 1}. ${CoreJassProvider.describePatch(patch)}") @@ -798,9 +799,7 @@ object SetupApp { if (patchTargets.isNotEmpty()) { log.info("Type `more` to browse supported patch targets.") } - if (versions.isNotEmpty()) { - log.info("Type `exact` to browse raw jass-history dump folders.") - } + log.info("Type `exact` to browse raw jass-history dump folders.") log.info("Enter a listed number, press Enter for the default, or type `more`.") while (true) { @@ -817,11 +816,11 @@ object SetupApp { title = "WC3 patch targets", versions = patchTargets, prompt = prompt, - exactVersions = versions + exactVersions = exactVersions )?.let { return it } "exact", "raw", "dumps" -> browsePatchVersions( title = "Exact jass-history dumps", - versions = versions, + versions = exactVersions, prompt = prompt, exactVersions = emptyList() )?.let { return it } diff --git a/src/main/kotlin/file/SetupMain.kt b/src/main/kotlin/file/SetupMain.kt index 0b3e45a..39927ec 100644 --- a/src/main/kotlin/file/SetupMain.kt +++ b/src/main/kotlin/file/SetupMain.kt @@ -21,6 +21,8 @@ class SetupMain { var gamePath: Path? = null + var gamePathOptedOut: Boolean = false + var requireConfirmation = false var noPJass = false diff --git a/src/test/kotlin/GenerateTests.kt b/src/test/kotlin/GenerateTests.kt index ec34844..8dcb751 100644 --- a/src/test/kotlin/GenerateTests.kt +++ b/src/test/kotlin/GenerateTests.kt @@ -396,6 +396,54 @@ class GenerateTests { Assert.assertTrue(setup.curatedDependencyIds.isEmpty()) } + @Test(priority = 10) + fun testGenerateWithNameUsesWizardPrompt() { + val setup = SetupMain() + setup.parseArgs(listOf("generate", "wizardproject")) + val answers = java.util.ArrayDeque(listOf("jass", "pre1.29", "none", "y", "y", "n")) + val prevPrompt = SetupApp.generatePrompt + try { + SetupApp.generatePrompt = { _, _ -> answers.removeFirst() } + Assert.assertTrue(SetupApp.prepareGenerate(setup)) + } finally { + SetupApp.generatePrompt = prevPrompt + } + + Assert.assertEquals(setup.commandArg, "wizardproject") + Assert.assertEquals(setup.scriptMode, ScriptMode.JASS) + Assert.assertEquals(setup.wc3Patch, CoreJassProvider.PRE_129_PATCH) + Assert.assertTrue(setup.gamePathOptedOut) + Assert.assertTrue(setup.addAgents) + Assert.assertTrue(setup.addGithubWorkflow) + Assert.assertTrue(setup.curatedDependencyIds.isEmpty()) + } + + @Test(priority = 10) + fun testGenerateWithNamePreservesCliSelectionsWhenWizardInputIsUnavailable() { + val setup = SetupMain() + setup.parseArgs( + listOf( + "generate", + "wizardproject", + "--with-agents", + "--with-ci", + "--with-dep", + "table-layout" + ) + ) + val prevPrompt = SetupApp.generatePrompt + try { + SetupApp.generatePrompt = { _, _ -> null } + Assert.assertTrue(SetupApp.prepareGenerate(setup)) + } finally { + SetupApp.generatePrompt = prevPrompt + } + + Assert.assertTrue(setup.addAgents) + Assert.assertTrue(setup.addGithubWorkflow) + Assert.assertEquals(setup.curatedDependencyIds, listOf("table-layout")) + } + @Test(priority = 10) fun testGenerateWizardRejectsUnsupportedScriptModeAndPatchInput() { val setup = SetupMain() diff --git a/templates/AGENTS.md b/templates/AGENTS.md index d3370ad..b0ca0ad 100644 --- a/templates/AGENTS.md +++ b/templates/AGENTS.md @@ -1,40 +1,42 @@ - + # AGENTS.md - WurstScript Map Project Notes WurstScript Warcraft III map project notes for editing `.wurst` code, dependencies, generated objects, tests, or map build logic. +## Read More On Demand + +This file is the working set; pull deeper docs into context only when the task needs them: + +- **Stdlib APIs**: grep `_build/dependencies/wurstStdlib2/wurst/` for wrappers and packages before writing a native call or new infrastructure. +- **Dependency guides**: before editing code that uses a dependency, check `_build/dependencies//` for its own `AGENTS.md` or usage guides and read them first (e.g. `wurst-table-layout` ships `AGENTS.md`, `AI_USAGE.md`, and `WC3_FRAMEHANDLE_GUIDE.md` — required reading before UI work). +- **Language details**: https://wurstlang.org/manual.html (full manual: generics, closures, modules, compiletime, operators). +- When unsure about syntax or local APIs, inspect nearby working code before guessing. + ## Working Rules - 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. -- 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. +- Make changes in the source package, not generated output. Do not edit `_build/` as source-of-truth; patch upstream dependency repos instead of copied dependency code. - 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. +- Fix compiler warnings unless they are intentionally suppressed. ## 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. +The most important coding rule: 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 wrapper or extension function exists (there is one for almost every native); grep the stdlib first. The only bar for a raw native is that you searched and confirmed no wrapper exists — then add a one-line comment saying so. Code that reads like JASS (manual handle juggling, native calls, global trigger callbacks, op-limit chunking) is wrong here even if it compiles. 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)`. +- Player state → `Player` extensions (`p.addGold`, `p.getId`, ...); prefer `players[i]` 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. +- 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: +Likewise, do not reinvent stdlib infrastructure — it is battle-tested against WC3 edge cases (recycling, op-limits, cleanup, desync) that hand-rolled versions get wrong. Grep for an existing system before building one: - Dummy spell casting → `DummyCaster` / `InstantDummyCaster` (unit pooling: `DummyRecycler`). - Triggered damage → `DummyDamage` to deal, `DamageEvent` to detect/modify. @@ -42,67 +44,36 @@ Keep custom engine-level infrastructure to a minimum. Stdlib packages are battle - 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. +If stdlib almost fits, wrap the stdlib type thinly and note why in a comment. Reinventing this is treated as a defect even if tests pass. ## Agent Workflow -Install dependencies: - ```bash -grill install -``` - -After Wurst changes, run quiet checks first: - -```bash -grill typecheck --quiet +grill install # install/update dependencies +grill typecheck --quiet # after Wurst changes grill test --quiet ``` -If quiet output reports a failure, rerun narrowly: - -```bash -grill typecheck -grill test PackageOrTestName -``` - -Use the failed file, line, package, or test name to narrow the next command. Avoid full noisy reruns unless there is no target. +If quiet output reports a failure, rerun narrowly using the failed file, line, package, or test name (`grill typecheck`, `grill test PackageOrTestName`). Avoid full noisy reruns unless there is no target. -For build changes: - -```bash -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. +For build changes: `grill build ExampleMap.w3x --quiet`. Builds default to production mode (compiletime `isProductionBuild()` returns `true`); add `--dev` only when validating behavior that needs `isProductionBuild() == false`. To dump a map's object-editor data to Wurst source: `grill exportobjects `. Done means relevant errors/warnings are fixed or explicitly explained. ## Project Configuration -`wurst.build` is the root YAML config. Key fields: `projectName`, `dependencies` (Git URLs managed by `grill`), and `buildMapData` (metadata written to the output `.w3x`). The default dependency is usually `wurstStdlib2`. Patch upstream repos instead of editing copied dependency code. +`wurst.build` is the root YAML config. Key fields: `projectName`, `dependencies` (Git URLs managed by `grill`), and `buildMapData` (metadata written to the output `.w3x`). The default dependency is usually `wurstStdlib2`. ## Lua vs Jass -Maps target Lua or Jass via World Editor settings. - -Lua mode: - -- No practical op-limit; long loops and deep calls are okay. -- `execute()` is a no-op for performance. Do not add it as an op-limit workaround. -- Use timers only when you need real asynchronous delay. +Maps target Lua or Jass via World Editor settings. Check the target before adding/removing `execute()` or timer chunking: -Jass mode: - -- The VM has an operation limit per thread. -- `execute()` resets the op counter by starting a new thread. -- Heavy work may need chunking across ticks. - -Check the target before adding/removing `execute()` or timer chunking. +- **Lua**: no practical op-limit; long loops and deep calls are fine. Do not add `execute()` as an op-limit workaround. Use timers only for real asynchronous delay. +- **Jass**: the VM has an operation limit per thread; `execute()` resets it by starting a new thread. Heavy work may need chunking across ticks. ## Wurst Essentials -Every `.wurst` file starts with a package: +Every `.wurst` file starts with a package; blocks are indentation-based (tabs or 4 spaces, never mixed): ```wurst package MyPackage @@ -112,10 +83,6 @@ init print("loaded") ``` -Blocks are indentation-based. Use tabs or 4 spaces consistently; do not mix. - -Common declarations: - ```wurst let immutable = 5 var mutable = 10 @@ -126,174 +93,50 @@ function max(int a, int b) returns int if a > b return a return b - -function doThing() - print("void functions omit returns") -``` - -Use `let` unless mutation is needed. Put locals near first use. Prefer obvious type inference. Do not write Jass-style `takes` / `returns nothing`. - -Control flow: - -```wurst -if x > y - ... -else if x < y - ... - -switch kind - case 1 - ... - default - ... - -while keepGoing - ... - -for i = 0 to 10 - ... - -for i = 10 downto 0 - ... - -for u in group - ... - -for u from group - ... -``` - -`continue` skips an iteration; `skip` is a no-op. Statements usually end at newline. Continue after `(`, `[`, operators, or before `.`, `..`, `)`, `]`, `begin`. - -Common operators: `+`, `-`, `*`, `/`, `div`, `%`, `mod`, `and`, `or`, `not`, `==`, `!=`, `<`, `<=`, `>`, `>=`. - -```wurst -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. - -### 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. - -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 +Use `let` unless mutation is needed. Put locals near first use. Prefer type inference. Do not write Jass-style `takes` / `returns nothing`. -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. +Control flow: `if`/`else if`/`else`, `switch x` + `case`/`default`, `while`, `for i = 0 to 10`, `for i = 10 downto 0`, `for u in group` / `for u from group`. `continue` skips an iteration; `skip` is a no-op statement. Statements end at newline; continue after `(`, `[`, operators, or before `.`, `..`, `)`, `]`, `begin`. -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: +Operators: `+`, `-`, `*`, `/` (real division, even on two ints), `div` (integer division), `%`, `mod`, `and`, `or`, `not`, `==`, `!=`, `<`, `<=`, `>`, `>=`, ternary `cond ? a : b`. -- 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: +Null-safe member access with `?.` skips the access (including argument evaluation) when the receiver is null; the receiver is evaluated once: ```wurst -if watcher != null - destroy watcher - watcher = null +target?.kill() // no-op when target is null +let owner = target?.getOwner() // null when target is null; chains: a?.next?.next ``` -### Table UI and layout dependencies +The receiver type must be nullable (class/interface/string/handle — not `int`/`real`/`boolean`). If the member's own type cannot represent null (e.g. `getCount()` returning `int`), the `?.` call is only valid as a standalone statement, not as a value — use an explicit `if x != null` there. `?.` is not assignable (`a?.x = 5` is invalid). -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. -- Class members are public by default; restrict with `private`/`protected`. -- Every package implicitly imports `Wurst` unless `NoWurst` is imported. -- `import public` re-exports names. Plain `import` does not. -- Avoid `initlater` unless breaking an unavoidable init cycle. -- Package initialization is top-to-bottom; imports initialize before importers. - -Naming: +- Package members are private by default; use `public` for exports. Class members are public by default; restrict with `private`/`protected`. +- Every package implicitly imports `Wurst` unless `NoWurst` is imported. `import public` re-exports names; plain `import` does not. +- Package initialization is top-to-bottom; imports initialize before importers. Avoid `initlater` unless breaking an unavoidable init cycle. -- packages/classes: `UpperCamelCase` -- tuples: `lowerCamelCase` -- functions/members/locals: `lowerCamelCase` -- top-level constants: `UPPER_SNAKE_CASE` +Naming: packages/classes `UpperCamelCase`; tuples, functions, members, locals `lowerCamelCase`; top-level constants `UPPER_SNAKE_CASE`. ## Preferred Wurst Style -Use cascade syntax for setup: +Use cascade syntax for setup and extension functions for readable APIs: ```wurst CreateTrigger() ..registerAnyUnitEvent(EVENT_PLAYER_UNIT_ISSUED_ORDER) ..addCondition(Condition(function cond)) ..addAction(function action) -``` - -Use extension functions for readable APIs: -```wurst public function unit.getX2() returns real return GetUnitX(this) ``` +Prefer `target?.damage(50.)` over `if target != null` + `target.damage(50.)` when the null case simply does nothing; keep the explicit check when the null case needs handling or the accessed value's type cannot be null. + Prefer `vec2` tuples over `location` handles unless required. Prefer polymorphism/data modeling over large `instanceof`/`typeId` chains. Avoid unchecked `castTo` unless proven safe. -Lambdas need a target type. Standalone inference does not work: +Lambdas need a target type — standalone inference does not work: ```wurst Predicate even = x -> x mod 2 == 0 @@ -302,59 +145,82 @@ doAfter(1.) -> print("later") ``` -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. +Important closure rule: locals captured by a closure are captured by value. Treat captured locals as read-only shared state: assigning to one inside a callback does not update the outer local that was captured. For shared mutable state, keep the state in a class instance or use `reference(value)`, then mutate `.val` and destroy the reference when finished. Lambdas used as `code` cannot take parameters or capture locals. ## Classes, Tuples, Generics -`new` objects generally need `destroy`. Tuples are value types and must not be destroyed. +`new` objects generally need `destroy`. Tuples are value types and must not be destroyed. `super(...)` must be the first constructor statement; overridden methods require `override`. Interfaces declare required methods; modules (`use`) inject reusable members. + +Prefer `T:` generics for performance-sensitive or instance-heavy containers (`class Box`); 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; avoid hardcoded new object IDs unless existing code intentionally does so. ```wurst -class Missile - function onCollide(unit u) +let value = compiletime(fac(5)) -class Fireball extends Missile - override function onCollide(unit u) - ... +@compiletime function createSpell() + new AbilityDefinitionMountainKingThunderBolt(SPELL_ID) + ..setName("Wurst Bolt") + ..presetDamage(lvl -> 400. + lvl * 100.) ``` -`super(...)` must be the first constructor statement. Overridden methods require `override`. +Generated objects must use real melee objects as bases, never other custom objects (custom bases compile into invalid or order-dependent data). Melee bases carry baggage — repair costs, upgrades/tech requirements, stock/bounty/food/race/classification fields, default abilities, art/sound/tooltips — so audit and explicitly clear inherited side effects per object family (prefer local helper presets that null known-dangerous fields, then layer intended fields). Regression tests for generated objects should assert the *absence* of known inherited side effects, not only the presence of new fields. -Interfaces declare required methods; modules inject reusable members: +## Production Pitfalls -```wurst -interface Listener - function onClick() +Recurring real-world failure modes; treat as a pre-edit checklist for non-trivial changes. -module HasOwner - player owner +### Integer overflow -class Button - use HasOwner -``` +WC3 `int` is 32-bit signed and wraps silently at ~2.1 billion. Easy to hit when multiplying or summing large game quantities (gold/worth totals, damage products, accumulated stats — aggregate worths routinely exceed ~46k, the square root of int-max). -Prefer `T:` generics for performance-sensitive or instance-heavy containers: +- Promote to `real` BEFORE multiplying: `a.toReal() * b`, never `(a * b).toReal()` (already overflowed). +- Same for running sums of products: `total += worth.toReal() * count * mult`. +- `/` is real division so it does not overflow, but its operands still can. Prefer `real` accumulators that fan in many large terms. + +### Closure capture is by value + +If a closure assigns to a local from an outer scope, the outer local is not updated. Do not assign a value to an outer local inside a callback and use that outer local afterwards — declare it inside the closure, or register follow-up handlers inside the same callback that creates the value: ```wurst -class Box - T value +// BUG: clicked is still null outside the build closure +framehandle clicked = null +dialog.build() -> + clicked = textButton("OK", 0.08, 0.024) +clicked.onClick() -> + doThing() + +// OK: keep creation and handler in the same closure +dialog.build() -> + let clicked = textButton("OK", 0.08, 0.024) + clicked.onClick() -> + doThing() ``` -Old `T` generics erase through integer casts and can share storage. +When a value genuinely must cross closure boundaries, use `reference(value)`, access `.val`, and `destroy` the reference when the owner is done — but prefer restructuring to avoid it. -## Compiletime and Objects +### Wurst object lifetime is manual -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. +Lua output is garbage-collected at the runtime level, but Wurst class lifetimes and destructors are still explicit. Objects created with `new`, stored closures/listeners, timers/callbacks, references, and collections usually need `destroy` when their owner is done — do not rely on "Lua will GC it" if an `ondestroy` cleans up state, callbacks, or nested objects. Conversely, do not double-destroy: instance ids can be reused and there is no generic "is destroyed?" check, so owners must null their own stale references: ```wurst -let value = compiletime(fac(5)) - -@compiletime function createSpell() - new AbilityDefinitionMountainKingThunderBolt(SPELL_ID) - ..setName("Wurst Bolt") - ..presetDamage(lvl -> 400. + lvl * 100.) +if watcher != null + destroy watcher + watcher = null ``` -Avoid hardcoded new object IDs unless the existing code intentionally does so. +### Custom UI work + +If the project uses `wurst-table-layout` / `TableUi`, read that dependency's `AGENTS.md`, `AI_USAGE.md`, and `WC3_FRAMEHANDLE_GUIDE.md` before editing UI (see Read More On Demand). Hard rules that hold regardless: + +- Load TOC files in `init` if needed, but do no actual frame work (create/move/size/show/reparent) during blocking map-load init — delay it with `doAfter(0.)` or later. +- Build frames under their eventual parent (`withParent(...)` or inside `dialogFrame(...).build() ->`); re-parenting after creation can desync visual and clickable areas. +- Keep root panels/dialogs in the 4:3 safe band with `placeSafe(...)` and declared dimensions. Never size/place UI from `BlzGetLocalClientWidth()/Height()` without guarding against zero/invalid values (minimized clients). +- Prefer building reusable hidden frame trees after map load, then show/hide/update them; do not create complex frames on demand mid-game or destroy/recreate framehandles during cleanup. +- Do not move or resize Blizzard default frames (chat/messages) to make room — bad coordinates and default-frame refreshes can crash/desync. +- Prefer table-wide defaults (e.g. `layout.defaultHalign(Align.CENTER)`) over per-row alignment calls. ## Tests @@ -370,27 +236,16 @@ Tests should be small, deterministic, self-contained, and assertion-driven. If q ## Formatting -- spaces around binary operators: `a + b` -- no space before call parentheses: `foo(1)` -- no spaces around `.` or `..` -- no spaces after `(` or `[` or before `)` or `]` -- comments use `// Comment` -- avoid manual horizontal alignment -- prefix intentionally unused variables with `_` - -Hot doc comments: +- spaces around binary operators: `a + b`; no space before call parentheses: `foo(1)` +- no spaces around `.`, `..` or `?.`; no spaces after `(`/`[` or before `)`/`]` +- comments use `// Comment`; doc comments `/** ... */` appear in autocomplete +- avoid manual horizontal alignment; prefix intentionally unused variables with `_` -```wurst -/** This appears in autocomplete. */ -``` +## Quick Pitfall Checklist -## Pitfalls - -- Wurst code must be inside `package`. -- Indentation defines blocks. +- Wurst code must be inside `package`; indentation defines blocks. - `array.length` is only the initial length. -- `new` objects and stored closure objects often need `destroy`. -- Lambdas need a known target type. -- Lambdas used as `code` cannot capture locals. - Varargs are limited by Jass's 31-argument limit. -- Fix compiler warnings unless they are intentionally suppressed. +- Closures capture locals by value: do not expect callback assignments to update the outer local; use a class or `reference(value)` for intentional shared mutation. +- Lambdas need a known target type; `code` lambdas cannot capture locals. +- `new` objects and stored closures usually need `destroy` (see Production Pitfalls).