Skip to content

Commit f8e8cf3

Browse files
authored
version agents and allow non prod build (#72)
1 parent eea8315 commit f8e8cf3

6 files changed

Lines changed: 166 additions & 4 deletions

File tree

README.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,13 @@ Use `build` to generate an output map according to `wurst.build` specifications.
7979
> grill build
8080
```
8181

82+
Use `--dev` to build the output map in run/development mode. This makes compiletime
83+
`isProductionBuild()` return `false` while still writing a map file.
84+
85+
```cmd
86+
> grill build ExampleMap.w3x --dev
87+
```
88+
8289
## How it works
8390

8491
### Wurst Installation

src/main/kotlin/file/CLICommand.kt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,11 @@ enum class GlobalOptions(val optionName: String = "", val argCount: Int = 0) {
3636
setupMain.measure = true
3737
}
3838
},
39+
DEV_BUILD("--dev") {
40+
override fun runOption(setupMain: SetupMain, args: List<String>) {
41+
setupMain.devBuild = true
42+
}
43+
},
3944
WITH_AGENTS("--with-agents") {
4045
override fun runOption(setupMain: SetupMain, args: List<String>) {
4146
setupMain.addAgents = true

src/main/kotlin/file/SetupApp.kt

Lines changed: 49 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import org.slf4j.LoggerFactory
1717
import org.eclipse.jgit.api.Git
1818
import java.awt.GraphicsEnvironment
1919
import java.net.URL
20+
import java.nio.charset.StandardCharsets
2021
import java.nio.file.Files
2122
import java.nio.file.Path
2223
import java.nio.file.Paths
@@ -31,6 +32,11 @@ object SetupApp {
3132

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

35+
internal const val AGENTS_TEMPLATE_VERSION = "2026-06-10"
36+
private const val AGENTS_TEMPLATE_MARKER_PREFIX = "<!-- WURST_AGENTS_TEMPLATE_VERSION:"
37+
private const val AGENTS_TEMPLATE_MARKER = "<!-- WURST_AGENTS_TEMPLATE_VERSION: $AGENTS_TEMPLATE_VERSION -->"
38+
private const val AGENTS_TEMPLATE_SOURCE_HINT = "WurstScript Warcraft III map project notes"
39+
3440
fun handleArgs(setup: SetupMain) {
3541
this.setup = setup
3642
DependencyManager.debug = setup.debug
@@ -144,6 +150,9 @@ object SetupApp {
144150
| --quiet Suppress wurst output; only print errors and final result
145151
| --debug Print full stack traces for troubleshooting
146152
|
153+
|Build options:
154+
| --dev Build with compiletime isProductionBuild() = false
155+
|
147156
|Generate options:
148157
| --script-mode lua|jass Script mode (default: lua)
149158
| --wc3-patch <patch> WC3 patch target: reforged, pre1.29, or jass-history version
@@ -796,13 +805,46 @@ object SetupApp {
796805

797806
private fun downloadAgentsMd(projectDir: Path) {
798807
try {
799-
val content = URL("https://raw.githubusercontent.com/wurstscript/WurstSetup/master/templates/AGENTS.md").readText()
800-
Files.write(projectDir.resolve("AGENTS.md"), content.toByteArray())
808+
val content = withAgentsTemplateMarker(URL("https://raw.githubusercontent.com/wurstscript/WurstSetup/master/templates/AGENTS.md").readText())
809+
Files.writeString(projectDir.resolve("AGENTS.md"), content, StandardCharsets.UTF_8)
801810
log.info("✔ AGENTS.md written.")
802811
} catch (e: Exception) {
803812
log.warn("⚠️ Could not download AGENTS.md: ${e.message}. Continuing without it.")
804813
}
805814
}
815+
internal fun withAgentsTemplateMarker(content: String): String {
816+
return if (content.contains(AGENTS_TEMPLATE_MARKER_PREFIX)) {
817+
content
818+
} else {
819+
"$AGENTS_TEMPLATE_MARKER\n$content"
820+
}
821+
}
822+
823+
internal fun agentsTemplateWarning(content: String): String? {
824+
val markerLine = content.lineSequence().firstOrNull { it.startsWith(AGENTS_TEMPLATE_MARKER_PREFIX) }
825+
if (markerLine == AGENTS_TEMPLATE_MARKER) {
826+
return null
827+
}
828+
if (markerLine != null) {
829+
return "AGENTS.md was generated from an older WurstSetup template ($markerLine). Consider refreshing it from templates/AGENTS.md and re-applying project-local notes."
830+
}
831+
if (content.contains(AGENTS_TEMPLATE_SOURCE_HINT)) {
832+
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."
833+
}
834+
return null
835+
}
836+
837+
private fun warnIfAgentsTemplateStale(projectDir: Path) {
838+
val agents = projectDir.resolve("AGENTS.md")
839+
if (!Files.exists(agents)) {
840+
return
841+
}
842+
try {
843+
agentsTemplateWarning(Files.readString(agents, StandardCharsets.UTF_8))?.let { log.warn("⚠️ $it") }
844+
} catch (e: Exception) {
845+
log.warn("⚠️ Could not inspect AGENTS.md template marker: ${e.message}")
846+
}
847+
}
806848

807849
fun writeCiWorkflow(projectDir: Path) {
808850
val workflowDir = projectDir.resolve(".github/workflows")
@@ -821,6 +863,10 @@ object SetupApp {
821863

822864
args.add("-build")
823865

866+
if (setup.devBuild) {
867+
args.add("-dev")
868+
}
869+
824870
if (setup.measure) {
825871
args.add("-measure")
826872
}
@@ -1002,6 +1048,7 @@ object SetupApp {
10021048
private fun handleUpdateProject(configData: WurstProjectConfigData) {
10031049
WurstProjectConfig.handleUpdate(setup.projectRoot, null, configData)
10041050
ensureCoreJassFiles(setup.projectRoot, configData.wc3Patch)
1051+
warnIfAgentsTemplateStale(setup.projectRoot)
10051052
}
10061053

10071054
val REPO_REGEX = Regex("(https?://)([\\w.@-]+)(/)([\\w,-_]+)/([\\w,-_]+)(.git)?((/)?)")

src/main/kotlin/file/SetupMain.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ class SetupMain {
1515

1616
var measure = false
1717

18+
var devBuild = false
19+
1820
var projectRoot: Path = SetupApp.DEFAULT_DIR
1921

2022
var gamePath: Path? = null

src/test/kotlin/GenerateTests.kt

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -249,6 +249,20 @@ class GenerateTests {
249249
setup2.parseArgs(listOf("generate", "myproject", "--no-agents"))
250250
Assert.assertFalse(setup2.addAgents)
251251
}
252+
@Test(priority = 10)
253+
fun testAgentsTemplateMarkerAndWarnings() {
254+
val marked = SetupApp.withAgentsTemplateMarker("# AGENTS.md\n")
255+
Assert.assertTrue(marked.startsWith("<!-- WURST_AGENTS_TEMPLATE_VERSION: ${SetupApp.AGENTS_TEMPLATE_VERSION} -->"))
256+
Assert.assertNull(SetupApp.agentsTemplateWarning(marked))
257+
258+
val oldMarked = "<!-- WURST_AGENTS_TEMPLATE_VERSION: 2026-01-01 -->\n# AGENTS.md\n"
259+
Assert.assertTrue(SetupApp.agentsTemplateWarning(oldMarked)!!.contains("older WurstSetup template"))
260+
261+
val unmarkedGenerated = "# AGENTS.md - WurstScript Map Project Notes\n\nWurstScript Warcraft III map project notes"
262+
Assert.assertTrue(SetupApp.agentsTemplateWarning(unmarkedGenerated)!!.contains("without a version marker"))
263+
264+
Assert.assertNull(SetupApp.agentsTemplateWarning("# Custom project notes\n"))
265+
}
252266

253267
@Test(priority = 10)
254268
fun testDebugFlag() {
@@ -264,6 +278,15 @@ class GenerateTests {
264278
Assert.assertTrue(setup.quiet)
265279
}
266280

281+
@Test(priority = 10)
282+
fun testDevBuildFlag() {
283+
val setup = SetupMain()
284+
setup.parseArgs(listOf("build", "ExampleMap.w3x", "--dev"))
285+
Assert.assertEquals(setup.command, CLICommand.BUILD)
286+
Assert.assertEquals(setup.commandArg, "ExampleMap.w3x")
287+
Assert.assertTrue(setup.devBuild)
288+
}
289+
267290
@Test(priority = 10)
268291
fun testGenerateWithoutNameUsesWizardPrompt() {
269292
val setup = SetupMain()

templates/AGENTS.md

Lines changed: 80 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
<!-- WURST_AGENTS_TEMPLATE_VERSION: 2026-06-10 -->
12
# AGENTS.md - WurstScript Map Project Notes
23

34
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`, `==
137138
let label = count == 1 ? "unit" : "units"
138139
```
139140

141+
## WurstScript Production Pitfalls
142+
143+
These are recurring real-world Wurst/Warcraft III failure modes. Treat this section as a pre-edit checklist for any non-trivial Wurst change.
144+
145+
### Closure capture is by value
146+
147+
Wurst closures capture locals by value. If a closure assigns to a local from an outer scope, the outer local is not updated.
148+
149+
Bug pattern:
150+
151+
```wurst
152+
framehandle clicked = null
153+
dialog.build() ->
154+
clicked = textButton("OK", 0.08, 0.024)
155+
clicked.onClick() -> // clicked is still null outside the build closure
156+
doThing()
157+
```
158+
159+
Safer pattern:
160+
161+
```wurst
162+
dialog.build() ->
163+
let clicked = textButton("OK", 0.08, 0.024)
164+
clicked.onClick() ->
165+
doThing()
166+
```
167+
168+
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:
169+
170+
```wurst
171+
let clickedRef = reference(null)
172+
dialog.build() ->
173+
clickedRef.val = textButton("OK", 0.08, 0.024)
174+
clickedRef.val.onClick() ->
175+
doThing()
176+
destroy clickedRef
177+
```
178+
179+
Prefer avoiding the cross-boundary mutable reference entirely when the handler can be registered inside the closure that creates the frame.
180+
181+
### Object generation base IDs carry baggage
182+
183+
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.
184+
185+
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:
186+
187+
- repair gold/lumber costs and repair time
188+
- melee upgrades used / researches available / tech requirements
189+
- stock, dependency, bounty, collision, food, race, target, and classification fields
190+
- default abilities, autocast/order strings, buffs, art, missile, sound, and tooltip fields
191+
192+
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.
193+
194+
### Wurst object lifetime is manual
195+
196+
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.
197+
198+
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:
199+
200+
```wurst
201+
if watcher != null
202+
destroy watcher
203+
watcher = null
204+
```
205+
206+
### Table UI and layout dependencies
207+
208+
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.
209+
210+
- 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.
211+
- 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`.
212+
- 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.
213+
- 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.
214+
- 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.
215+
- 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.
216+
- 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.
217+
- Hide and reuse multiplayer UI frame trees. Do not destroy/recreate framehandles during gameplay cleanup.
140218
## Packages and API Shape
141219

142220
- Package members are private by default; use `public` for exports.
@@ -182,7 +260,7 @@ doAfter(1.) ->
182260
print("later")
183261
```
184262

185-
Closures capture locals by value. Stored/object-backed closures often need cleanup. Lambdas used as `code` cannot take parameters or capture locals.
263+
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.
186264

187265
## Classes, Tuples, Generics
188266

@@ -223,7 +301,7 @@ Old `T` generics erase through integer casts and can share storage.
223301

224302
## Compiletime and Objects
225303

226-
Use compiletime generation for object-editor data. Prefer wrappers and ID generators so IDs stay stable and collision-free.
304+
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.
227305

228306
```wurst
229307
let value = compiletime(fac(5))

0 commit comments

Comments
 (0)